buto > /dev/null

だいたい急に挑戦してゴールにたどり着かずに飽きる日々です

Java DynamoDB接続

久しぶりにDynamoDB使ってみる

半年前くらいにちょこっと仕事で使ったDynamoDB。費用が安いNoSQLデータベースってことしか分かってないけど

昨日作ったRESTAPIからDynamoDBにデータ登録してみます

DynamoDBテーブル作成

AWSマネジメントコンソールでAPIからデータを登録するテーブルを作成する

ちょっとした日記(嬉しかったこと、悲しかったこと)を記録するアプリを想定しています

記録内容を保持するjournalテーブルを作成しました

試しに「アプリ開発をスタートしたよ」という嬉しかった出来事(eval=10)のデータを入れています(AWSマネジメントコンソールで登録) f:id:butorisa:20201227220256p:plain

APIからDB接続するためのアクセスキーを作成

JavaAPI)から先ほど作成したDynamoDBテーブルにアクセスするためのアクセスキーを作成する

AWSマネジメントコンソールにてIAM-ユーザーでアクセスキーの作成ボタンを押下すればキーが作成される

AWS CLIの設定ファイルにアクセスキーを記述

AWS CLIのインストール手順はこちら

brewでAWS CLIのインストール

インストールすると~/.aws/configという設定ファイルが作成されるのでこのファイルに先ほどのアクセスキーを記述する

[default]
aws_access_key_id=アクセスキー
aws_secret_access_key=シークレットアクセスキー
region=us-east-1

JavaからDynamoDBにアクセスする

build.gradle
dependencies {
    /* dynamoDB */
    implementation platform('com.amazonaws:aws-java-sdk-bom:1.11.926')
    implementation 'com.amazonaws:aws-java-sdk-dynamodb'
}
application.yml

credential.profile:defaultは先ほど編集した~/.aws/configファイルのdefault部分を指します

amazon:
  dynamodb:
    endpoint: https://dynamodb.ap-northeast-1.amazonaws.com
  credential:
    profile: default
DBConfigクラス

こちらの記事のクラスをコピペしたが、@EnableDynamoDBRepositoriesが見つからずエラーになるので一旦消す

Spring Data DynamoDBでSpringからDynamoDBにアクセスする

Controllerクラス
package com.calico.controller;

import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.model.PutItemRequest;
import com.amazonaws.services.dynamodbv2.model.PutItemResult;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class JournalController {

    private static AmazonDynamoDB amazonDynamoDB = AmazonDynamoDBClientBuilder.standard().build();

    /**
     * 記録の登録
     *
     * @param request 記録(画面入力値)
     * @return response 登録結果
     */
    @RequestMapping(value = "/journals", method = RequestMethod.POST)
    public PutItemResult postJournal(@RequestBody journalRequest request) {
        Map<String, AttributeValue> registerData = new HashMap<>();
        registerData.put("user_id", new AttributeValue().withS(request.user()));
        registerData.put("category", new AttributeValue().withS(request.category().name()));
        registerData.put("eval", new AttributeValue().withS(String.valueOf(request.eval())));
        registerData.put("memo", new AttributeValue().withS(request.memo()));

        long currentTime = System.currentTimeMillis();
        registerData.put("register_date", new AttributeValue().withS(String.valueOf(currentTime)));
        registerData.put("update_date", new AttributeValue().withS(String.valueOf(currentTime)));

        // データ登録処理
        PutItemResult registerResult = amazonDynamoDB.putItem(new PutItemRequest("journal", registerData));
        return registerResult;
    }

    /**
     * 記録登録APIリクエストRecords
     *
     * @param user     ユーザーID
     * @param category 記録カテゴリー
     * @param eval     記録内容のポジティブ度(0-10 ポジティブになるほど値が大きくなる)
     * @param memo     記録内容
     */
    public record journalRequest(String user, Categories category, int eval, String memo) {
    }

    /**
     * 記録登録APIレスポンスRecords
     */
    public record journalResponse(String user, Categories category, int eval, String memo) {
        public String getUser() {
            return user;
        }

        public Categories getCategory() {
            return category;
        }

        public int getEval() {
            return eval;
        }

        public String getMemo() {
            return memo;
        }
    }

    /**
     * 記録カテゴリー
     */
    public enum Categories {
        Home,
        Job,
        Community,
        Other
    }
}

そしてハマる。

ビルドは通るのですが、SpringBootアプリケーションを実行するとDBドライバ設定がされてないエラーが出ます

application.ymlにspring.datasourceを設定すれば良さそうだけど、お手本が見つからない…明日調べます

Description:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

Reason: Failed to determine a suitable driver class


Action:

Consider the following:
    If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
    If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).

Java Recordsの使いドコロを探る

Java15にスキルをアップデート

未だに仕事ではJava8を使い続けているのでそれにかまけてJavaキャッチアップできてなかった…

これからも仕事、趣味共にバックエンドはJavaを使っていくことになりそうなので今日久しぶりに最新Javaを調べてみた!

(資格もJavaGold11にアップデートしなきゃ)

新しいデータの扱い方 Records

JDK 14でPreview ReleaseされるRecordsが素敵なのでJShellで試そう

前から存在は知っていたのですが、試したことなかったのでRecordsを使ってみます!

getter/setterの記述不要でコンストラクタでフィールドに値をセットしてくれる仕組みなのかー

RESTAPIで使ってみた

私はJavaAPI開発で使うことが多いのでSpringBootのRESTAPIでRecordsを試してみました

(家事や仕事で気づいたこと、嬉しかったことなどを記録するアプリを想定)

  • 開発環境
    • MacbookAir
    • OracleJDK15
    • SpringBoot2.4.1

JDK15でもRecordsはプレビューリリースなのでbuild.gradleに以下の設定を記述しないとrecordsがコンパイルエラーになります

tasks.withType(JavaCompile) {
    options.compilerArgs += ['--enable-preview']
}

またIDEの実行環境設定でJVM引数が必要です f:id:butorisa:20201226235935p:plain

お試しなのでコントローラーにRecords書いちゃってます(本来はモデルクラスに書くべき?)

package com.calico.controller;

import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class JournalController {

    /**
     * 記録の登録
     *
     * @param request 記録(画面入力値)
     * @return response 登録結果
     */
    @RequestMapping(value = "/journals", method = RequestMethod.POST)
    public journalResponse postJournal(@RequestBody journalRequest request) {
        journalResponse response = new journalResponse(1, request.category(), request.eval(), request.memo());
        // TODO データ登録処理
        return response;
    }

    /**
     * 記録登録APIリクエストRecords
     * @param category 記録カテゴリー
     * @param eval 記録内容のポジティブ度(0-10 ポジティブになるほど値が大きくなる)
     * @param memo 記録内容
     */
    public record journalRequest(Categories category, int eval, String memo) {
    }

    /**
     * 記録登録APIレスポンスRecords
     */
    public record journalResponse(int id, Categories category, int eval, String memo) {
        public int getId() {
            return id;
        }

        public Categories getCategory() {
            return category;
        }

        public int getEval() {
            return eval;
        }

        public String getMemo() {
            return memo;
        }
    }

    /**
     * 記録カテゴリー
     */
    public enum Categories {
        Home,
        Job,
        Community,
        Other
    }
}

APIのリクエスト、レスポンスにRecordsを使っています

ちなみに実行結果はこちらです まだDB接続していないのでリクエストをそのままレスポンスするだけです f:id:butorisa:20201226235801p:plain

ここで注意なのですが、レスポンスのRecordsはgetterが必要です

getterを書かないと以下のエラーが発生します

org.springframework.http.converter.HttpMessageNotWritableException: No converter found for return value of type

これは@RestControllerをつけているAPIメソッドではSpringBootがレスポンスをjsonに変換する時にgetCategory()メソッドが必要なのに定義されていないからっぽい

Recordsが暗黙で用意するメソッドはgetCategory()ではなく、プロパティ名ままのcategory()だからSpringBootはgetterと認識してくれないみたい。。。

SpringBootがRecords対応してくれたらすごく便利かも!!

Raspberry Pi入門〜Pythonで電源ランプ制御〜

ほったらかしラズパイをやっと動かしてみた

PyConJPで電子工作のセッションを聞いて勢いでラズパイを買ったものの、買って満足してしまい。。。

今年もPyLadiesアドベントカレンダー参加したいなあ、と思っていたのでこのタイミングでラズパイ始めました!

この記事ではPythonコードからRaspberry Piの電源ランプをチカチカさせます

秋葉原の書泉で買ったスターターキットと入門書(付属のSDカードはRaspbianインストール済)

スターターキットの詳細はこちら f:id:butorisa:20201206220112j:plain

準備.ラズパイ起動

ラズベリーパイをモニター、マウス、キーボードと繋げて電源に接続 f:id:butorisa:20201206221647j:plain

モニターが虹色に(壊れたのかと思った・・・)少し経つとラズベリーf:id:butorisa:20201206221356j:plain f:id:butorisa:20201206221337j:plain

Raspbianのホーム画面が表示された!画面から日本語設定、wi-fi設定をして準備完了! f:id:butorisa:20201206221745j:plain

電源ランプの設定ファイルを編集

ここからはこの記事をお手本にしました

Raspberry Pi 3の電源ランプ、アクセスランプでLチカをする

※初心者のため説明に誤りがあるかもしれません。。。

写真左下にある2つのランプを制御します

  • 上がSDカード読み込み時に点灯する緑ランプ(led0)
  • 下が電源接続時に常に点灯している赤ランプ(led1)

f:id:butorisa:20201206223752j:plain

右のトゲトゲ(コネクタピン)には番号があり、この20番と22番の出力命令を上のランプにそれぞれ流すようにランプの設定ファイルに追記する

※電源を消して再度つけた時はこのコマンドを実行しないと以降のpythonが効かないので注意 f:id:butorisa:20201206224529j:plain

sudo su
echo gpio > /sys/class/leds/led0/trigger
echo 20 > /sys/class/leds/led0/gpio
echo gpio > /sys/class/leds/led1/trigger
echo 22 > /sys/class/leds/led1/gpio
exit

ランプを制御するPythonコードを記述

20番、22番ピンにランプをつける(GPIO.HIGH)消す(GPIO.LOW)の命令をする

1秒間隔で点灯・消灯を繰り返すようにしています

import RPi.GPIO as GPIO
from time import sleep

Green=20
Red=22

GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
GPIO.setup(Green, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(Red, GPIO.OUT, initial=GPIO.LOW)

try:
    while True:
        GPIO.output(Green, GPIO.HIGH)
        sleep(1)
        GPIO.output(Green, GPIO.LOW)
        sleep(1)
        GPIO.output(Red, GPIO.HIGH)
        sleep(1)
        GPIO.output(Red, GPIO.LOW)
        sleep(1)

except KeyboardInterrupt:
    pass

GPIO.cleanup()

このコードをpyファイルにしてsudoで実行すればランプがチカチカ!

sudo python3 ファイル名.py

f:id:butorisa:20201206234309g:plain

React Nativeを始めてみた

やっとReact Native始めました

夏頃からReact Nativeやるぞ〜と言って早数ヶ月…すっかり寒くなってしまった

今日やっとReact Nativeに入門しました!!

React NativeはJavaScriptフレームワークReactのモバイル版で、1つのコードでiOSAndroidどちらでも動くクロスプラットフォームが実現できる!

(Reactやったことないんだが、大丈夫かな)

開発環境を構築する

React Native環境構築手順

このQiita記事の通りにやっただけですが、書いていきます

また、今回はAndroid環境のみです

  • macOS

  • nodev12.18.2

  • npm6.14.5

  • AndroidStudio

  • OPPO R15 Neo

React Nativeをインストール

既にNode.js、npmはインストール済みだったので

Homebrewでファイルのバージョン管理ライブラリ?のwatchmanをインストール

(Qiitaに書いてあったからインストールしました これはなくても良いのかも)

brew update
brew install watchman

npmでReact Nativeをインストール

npm install -g react-native-cli

AndroidStudioの設定

私の環境ではOreoのみ入っていたので、QiitaにしたがってMarshmallowを追加しました

Google APIなどはデフォルトで入っているようだったので特に設定していません

あとはAndroidSDKにパスを通すだけ

vi ~/.bash_profile

# .bash_profileの末尾に以下を追加して保存
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/tools
export PATH=$PATH:$ANDROID_HOME/platform-tools

React Nativeプロジェクトを作成

あとはコマンドでプロジェクトを作成して実行するだけ!

以下のコマンドでReact Nativeプロジェクトを作成します

# 事前にプロジェクトを作るディレクトリに移動しておく
react-native init プロジェクト名

React Nativeプロジェクト実行!

プロジェクトが作成されたら、AndroidStudioを起動した状態でスマホを接続

スマホのUSBデバッグを有効にしておきます

AndroidStudio画面上部に接続したスマホが表示されていればOK f:id:butorisa:20201107190813p:plain

それでは実行!

cd プロジェクト名
react-native run-android

ん!?openjdkの開発元を検証できないため開ません!?

システム環境設定-セキュリティ-一般からopenjdkを許可すればいいみたい

これで再度react-native run-androidを実行するとプロジェクトのビルドが成功!

スマホにこんな画面が表示されました f:id:butorisa:20201107191757p:plain

Kotlin入門 おみくじアプリを作ってみた

簡単なアプリを作りました

アプリを起動時 ボタンを押すとおみくじ結果画面に遷移します

f:id:butorisa:20201020175453p:plain

おみくじ結果画面 3秒ほど「Fortune Telling...」と表示され、結果が表示されます

f:id:butorisa:20201020175519p:plain f:id:butorisa:20201020175528p:plain

githubで公開しています Kotlin学習にどうぞ! https://github.com/butorisa/fortune-app

※画像には表示されていませんが、利用しているAPIのリンク・占い提供元のリンクを3枚目画面の下部に表示するようにしました

アプリの概要

  • 開発端末OS:Android 8.1.0(Oreo)
  • 利用APIWeb ad Fortune 無料版API
  • HTTP通信ライブラリ:OkHttp3

  • MainActivity

    • ActionBar(正確にはToolBar)のみ
    • 画面部分はFragmentで描画
  • EntryFragment
    • 起動時の画面(1枚目)
    • MainActivityの上から描画
    • FortuneResultFragmentに遷移するボタンを保持
  • FortuneResultFragment
    • おみくじ結果描画画面(2、3枚目)
    • 画面描画時に占いAPIを実行して、結果の12星座運勢からランダムな結果を表示
    • API実行はCoroutineで非同期実行
    • おみくじ結果が表示されるまで[GO BACK]ボタンは動作しない
    • ボタン押下で前画面へ戻る(EntryFragment指定はしていない)
  • FortuneResultBean
  • RestApiClient
    • 占いAPI呼び出しクラス(バックエンド)
    • 引数にURLを取り、APIを実行、レスポンスjson(String型)を返す
    • BeanクラスへのマッピングはRestApiClient呼び出し元で行う

MainActivity

MainActivityはActionBarしかない空っぽの画面なのでEntryFragmentを表示するだけ アプリテーマを「NoActionBar」にしているのでActionBarの設定コードを書いている

    /**
     * Activity描画
     */
    @RequiresApi(Build.VERSION_CODES.O)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // actionbar
        val toolbar = findViewById<Toolbar>(R.id.toolbar_fortune)
        toolbar.title = "pretty chipper"
        setSupportActionBar(toolbar)

        // EntryFragment描画
        val transaction = supportFragmentManager.beginTransaction()
        transaction.addToBackStack(null)
        transaction.replace(R.id.container, EntryFragment())
        transaction.commit()
    }

EntryFragment

inflater.inflate() でFragmentのxmlファイルをバインド ボタンに押下時の処理を設定

    /**
     * EntryFragment描画
     */
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        super.onCreateView(inflater, container, savedInstanceState)
        val view = inflater.inflate(R.layout.fragment_entry, container, false)

        // ボタン押下でFortuneResultFragmentに移動
        view?.findViewById<Button>(R.id.button_telling_fortune)?.setOnClickListener {
            val transaction = fragmentManager?.beginTransaction()
            transaction?.addToBackStack(null)
            transaction?.replace(R.id.container, FortuneResultFragment())
            transaction?.commit()
        }
        return view
    }

FortuneResultFragment

onCreateView()で画面描画時にCoroutineを使って非同期で占いAPIを実行 APIのURLには「yyyy/MM/dd」形式で現在日付をつけると本日の占い結果が返却される APIレスポンスが返ってきたら、TextViewに占い結果を設定 レスポンスjsonを全てgetJSONObject()で取得するのは面倒なので、12星座の運勢のレスポンス部分だけBeanクラスを作りマッピング

    /**
     * FortuneResultFragment描画
     */
    @RequiresApi(Build.VERSION_CODES.O)
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        super.onCreateView(inflater, container, savedInstanceState)
        val view = inflater.inflate(R.layout.fragment_fortune_result, container, false)

        /* Coroutine start */
        this.lifecycleScope.launch {
            // call API
            withContext(Dispatchers.Default) {
                val url = "http://api.jugemkey.jp/api/horoscope/free/" + getCurrentDate()
                RestApiClient().requestGet(url)
            }.let {
                delay(2000)
                // mapping json to bean
                val fortuneResult = parseJson(getCurrentDate(), it)
                // set TextView
                view?.findViewById<TextView>(R.id.fortune_content)?.text =
                    fortuneResult.get((0..11).random())?.content // TODO ランダムな星座を表示

                // ボタン押下で前画面へ移動
                view.findViewById<Button>(R.id.button_back_to_entry).setOnClickListener {
                    fragmentManager?.popBackStack()
                }
            }
        }
        /* Coroutine end */
        return view
    }

    /**
     * 現在日付取得
     * @return 現在日付(yyyy/MM/dd)
     */
    @RequiresApi(Build.VERSION_CODES.O)
    fun getCurrentDate(): String {
        val today = LocalDate.now()
        val dateFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd")
        return dateFormatter.format(today)
    }

    /**
     * json→FortuneResultBean変換
     * @param today:現在日付(yyyy/MM/dd)
     * @param strJson:レスポンスjson
     * @return 運勢一覧(FortuneResultBeanリスト)
     */
    private fun parseJson(today: String, strJson: String): List<FortuneResultBean> {
        val json = JSONObject(strJson)
        // remove "\" from json-property
        val strResult = json.getJSONObject("horoscope").getString(today.replace("\\", ""))
        return ObjectMapper().readValue(strResult, object: TypeReference<List<FortuneResultBean>>(){})
    }

FortuneResultBean

占いAPIの運勢部分のjsonマッピングするBeanクラス このBeanクラスを12星座分のリストにして受け取る

/**
 * 占いAPIレスポンスBean
 */
class FortuneResultBean {
    @JsonProperty("content")
    var content: String? = null

    @JsonProperty("money")
    var money: String? = null

    @JsonProperty("job")
    var job: String? = null

    @JsonProperty("love")
    var love: String? = null

    @JsonProperty("total")
    var total: String? = null

    @JsonProperty("item")
    var item: String? = null

    @JsonProperty("color")
    var color: String? = null

    @JsonProperty("day")
    var day: String? = null

    @JsonProperty("rank")
    var rank: String? = null

    @JsonProperty("sign")
    var sign: String? = null
}

RestApiClient

OkHttp3を利用してGETリクエスト レスポンスボディをString型のまま呼び出し元へ返す

    /**
     * GETリクエスト
     * @param APIリクエストURL
     * @param リクエストパラメータ
     * @return APIレスポンス(String)
     */
    @RequiresApi(api = Build.VERSION_CODES.N)
    fun requestGet(url: String, vararg param: String?): String {
        val request = Request.Builder().url(url!!).build()
        val httpClient = OkHttpClient()
        try {
            val response = httpClient.newCall(request).execute()
            return response.body!!.string()

        } catch (e: IOException) {
            // TODO handle exception
            e.printStackTrace()
        }
        // TODO Handle Exception
        throw NullPointerException()
    }

    companion object {
        private val JSON: MediaType = "application/json; charset=utf-8".toMediaTypeOrNull() as MediaType
    }

きれいに書くよう意識しました

今までとりあえず動くことだけ意識していたのですが、後で見返すと分かりづらい。。。 FragmentにあるボタンのクリックリスナーをActivityに書いていたり、API呼び出し関数にUI操作を書いていたり、不要な変数を宣言していたり… このあたりをスッキリするようにコーディングしてみました Androidのコード設計、デザインパターンも意識しながらコーディングできるようになりたい!

Docker ElasticSearchでPDF全文検索

ElasticSearchイメージでサクッと全文検索してみる!!

ElasticSearch環境構築

  • 環境
#elasticsearchイメージをダウンロード
docker pull elasticsearch
# コンテナ作成・起動
docker run -it -d --name elastic_search -p 9200:9200 -p 9300:9300 -v C:¥Users¥buto¥share:/mnt/share elasticsearch

全文検索に使うプラグインをインストール

# コンテナに入る
docker exec -it elastic_search /bin/bash

###### ここからコンテナ環境 ######

# ログイン直後はrootユーザー
su - elasticsearch
# elasticsearch-pluginコマンドでプラグイン操作
elasticsearch-plugin install kuromoji # 日本語対応
elasticsearch-plugin install analysis-icu # 文字の解析器
elasticsearch-plugin install ingest-attachment # ESにファイルを取り込む
# プラグインが追加された
elasticsearch-plugin list
# analysis-icu
# analysis-kuromoji
# ingest-attachment

プラグインをElasticSearchに反映させるためにコンテナ再起動(Docker for Windows GUIから実行) ※ コンテナ内でsudo systemctl elasticsearch restartできなかった。。。

PDFを取り込むパイプラインを作成

以下のリクエストでPDFをElasticSearchに流すパイプラインを作成する

curl --location --request PUT 'http://localhost:9200/_ingest/pipeline/attachment'
--header 'Content-type: application/json'
--data-raw '{
  "description" : "sample mapping",
  "processors" : [
    {
      "attachment" : {
        "field" : "data",
        "indexed_chars" : -1,
        "properties" : [
         "content",
         "content_type"
        ]
      }
    }
  ]
}'

attachmentという名前のパイプラインができる!

文字解析器を作成

PDFの文章を2~3文字ずつ区切って解析するよう設定

curl --location --request PUT 'http://localhost:9200/ngram_1'
--header 'Content-type: application/json'
--data-raw '{
    "settings": {
        "analysis": {
            "analyzer": {
                "my_ja_analyzer": {
                    "type": "custom",
                    "tokenizer": "my_tokenizer",
                    "char_filter": [
                        "icu_normalizer",
                        "kuromoji_iteration_mark"
                    ],
                    "filter": [
                        "kuromoji_baseform",
                        "kuromoji_part_of_speech",
                        "ja_stop",
                        "kuromoji_number",
                        "kuromoji_stemmer"
                    ]
                }
            },
            "tokenizer": {
                "my_tokenizer": {
                    "type": "ngram",
                    "min_gram": 2,
                    "max_gram": 3,
                    "token_chars": [
                        "letter",
                        "digit"
                    ]
                }
            }
        }
    },
    "mappings": {
        "properties": {
            "attachment.content": {
                "type": "text",
                "analyzer": "my_ja_analyzer",
                "search_analyzer": "my_ja_analyzer"
            }
        }
    }
}'

ngram_1という名前の解析器が作成される

PDFデータをBase64エンコード

HTTPリクエストボディでPDFをパイプラインに送るため、PDFファイルをBase64エンコードする ElasticSearchのVMLinux)で以下のコマンドを実行

base64 -w 0 sample.pdf > sample.txt

sample.txtにBase64エンコードした内容が出力される

PDFをパイプラインに投入

curl --location --request POST 'http://localhost:9200/ngram_1/_doc/1?pipeline=attachment'
--header 'Content-type: application/json'
--data-raw '{
    "index": {
        "_index": "ngram",
        "_type": "pdffile",
        "_id": "1",
        "pipeline": "attachment"
    },
    "data": "sample.txtの中身"
}'

これでElasticSearchにPDFの文章が取り込まれる!

文字列検索してみる

GETリクエストのqパラメータに検索したい文字列をセットする

curl --location --request GET 'http://localhost:9200/ngram_1/_search?q=検索ワード'

ヒットするとこんなレスポンスが返ってきます

{
    "took": 34,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 1,
            "relation": "eq"
        },
        "max_score": 2.7496266,
        "hits": [
            {
                "_index": "ngram_1",
                "_type": "_doc",
                "_id": "1",
                "_score": 2.7496266,
                "_source": {
                    "data": "sample.txtの中身",
                    "attachment": {
                        "content_type": "application/pdf",
                        "content": "sample.txtをBase64デコードした内容"
                    },
                    "index": {
                        "pipeline": "attachment",
                        "_index": "ngram",
                        "_type": "pdffile",
                        "_id": "1"
                    }
                }
            }
        ]
    }
}

ヒットしなかった場合はこんなレスポンス

{

    "took": 6,
    "timed_out": false,
    "_shards": {
        "total": 1,
        "successful": 1,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 0,
            "relation": "eq"
        },
        "max_score": null,
        "hits": []
    }
}

やってみて

ElasticSearchのDockerイメージがあったので構築は楽でした! PDFを取り込むためのパイプライン、解析器の作成などが理解できず時間が掛かりました この設定で大体の日本語はヒットするのですが、ヒットするべきでないワードでもヒットしてしまうことが多々ありました… AWSのElasticsearchServiceもありますが、あちらはanalysis-icuがサポートされていなかったので、ヒットしてほしい時にヒットできないことがありました(AWSの方はプラグイン追加はできない) 全文検索をちょっと試してみたい時にどうぞ!!

Ruby インクリメント演算子が使えない理由

昨日、夜中の空腹をまぎらわせるためにRubyをインストールしてみました 今朝から制御構文などの基礎コードを書いていたのですが、num++のような インクリメント演算子が使えないことが分かり「なんで?」と気になったので調べました

理由:Rubyでは数値もオブジェクトだから

こちらのブログで分かりました! Ruby にインクリメント演算子のようなものが無い理由

インクリメント演算子が利用できない理由をRuby開発者が以下のように語っています

[ruby-list:5323] Re: Questions on specs and threads

3) 記号的な記法
これは単なる私の趣味ですが, 単項インクリメントとかがたまに欲しく
なります. i += 1 でいいわけですが. i++ と書いて怒られる (^^;

すんません.この件は以前から指摘されているのですが(演算子はC に似ているのに++と--は対応する演算子が無い),++の動作が本質 的に「変数を操作する」ものであるため,変数がオブジェクトでな いRubyでは導入できないでいます.++や--の「オブジェクト指向的 意味」がRubyの他の部分と整合性を保ったまま定義できれば採用し たいのですが….

Rubyは変数の操作をさせたくない言語なんですね 「変数がオブジェクトでない」ってどういうことなんだろう??

Rubyでは変数がオブジェクトでない」とは

【Ruby公式サイト】JavaからRubyへより

Javaと違って、Rubyは… 2や3.14159といった数値も含めて、すべてのものはオブジェクトです。

変数numは(numそのものが)オブジェクトではなくて、数値オブジェクトへの参照 → 1つの数値オブジェクトを共有している

# 変数numには123そのものではなく
# 123へのアドレスが格納されている

num = 123
puts num.object_id # 247が出力される

num_cp = 123
puts num_cp.object_id # 247が出力される

変数はオブジェクトへの参照だから、「変数自体がオブジェクトでない」ってことなんですね

数値オブジェクトはimmutable

上記コードのようにnumnum_cpは1つの数値オブジェクトを参照している → 数値オブジェクト「123(object_id:247)」自体が操作されたら、   参照している変数の値が全部変わっちゃう!!

なので、数値オブジェクトはimmutable(不変、final)になっている 以下のコードは変数の値を操作しているように見えるが、参照先を付け替えているだけ

cnt = 123
puts cnt.object_id # 247が出力される

cnt = 456
puts cnt.object_id # 913が出力される

参照している数値オブジェクトを付け替えることによって数値オブジェクト自体は操作されずに 変数の値が操作されているように見える!

インクリメント演算子は数値オブジェクトの操作を意味する

もう一度、最初に引用した開発者のコメントを見てみます

++の動作が本質的に「変数を操作する」ものであるため,変数がオブジェクトでな いRubyでは導入できないでいます.

インクリメント演算子は数値オブジェクト自体に加算することを意味するので、 Rubyの「数値オブジェクトはimmutable」のルールと合わなくてサポートされていない

言語って奥が深い!!

Rubyでインクリメント演算子がサポートされていないことにもちゃんと理由があった! Pythonも同じ理由でインクリメント演算子をサポートしていないらしい(使えると勘違いしていた)

このテーマとは違いますが、調べている時に「各言語のメモリの使い方」の解説に遭遇して とても面白かったです メモリとスタックとヒープとプログラミング言語