Vespa: ドキュメントの操作

今回は、curlでドキュメントの追加、更新、検索、削除をしてみようと思います。Dockerとvespa-cliが準備済みの前提で話を進めます。

まず、今回、利用するアプリケーションを用意します。適当なディレクトリを作成して、

  • services.xml
  • schemas/doc.sd

のファイルを作成します。

services.xmlは以下の内容で作成します。

<?xml version="1.0" encoding="UTF-8"?>
<services version="1.0" xmlns:deploy="vespa" xmlns:preprocess="properties">
  <container id="default" version="1.0">
    <search></search>
    <document-api></document-api>
    <nodes>
      <node hostalias="node1"></node>
    </nodes>
  </container>
  <content id="mind" version="1.0">
    <min-redundancy>1</min-redundancy>
    <documents>
      <document type="doc" mode="index"/>
    </documents>
    <nodes>
      <node hostalias="node1" distribution-key="0" />
    </nodes>
  </content>
</services>

schemas/doc.sdは以下です。

schema doc {
    document doc {
        field doc_id type string {
            indexing: summary | attribute
            attribute: fast-search
        }
        field title type string {
            indexing: index | summary
            index: enable-bm25
        }
        field content type string {
            indexing: index | summary
            index: enable-bm25
        }
    }

    fieldset default {
        fields: title, content
    }

    rank-profile default {
        first-phase {
            expression: nativeRank(title, content)
        }
    }
}

シンプルなスキーマですが、doc_id、title、contentのフィールドを作成して、titleとcontentはbm25で検索できるようにしてます。

2つのファイルが準備できたら、Vespaを起動します。

$ docker run --detach --name vespa --hostname vespa-container --publish 8080:8080 --publish 19071:19071 vespaengine/vespa

起動したら、2つのファイルを作成したディレクトリでデプロイを実行します。

$ vespa deploy --wait 300
Waiting up to 5m0s for deploy API…
Uploading application package… done

Success: Deployed . with session ID 2
Waiting up to 5m0s for deployment to converge…
Waiting up to 5m0s for cluster discovery…
Waiting up to 5m0s for container default…

みたいな感じで、デプロイされます。

早速、ドキュメントを追加します。ChatGPTにブログっぽい記事のドキュメントを3つ作ってもらったので、以下を登録します。

$ curl -X POST "http://localhost:8080/document/v1/fess/doc/docid/blog-1" \
     -H "Content-Type: application/json" \
     -d '{
         "fields": {
             "doc_id": "blog-1",
             "title": "Exploring the Beauty of Nature",
             "content": "Nature has always been a source of solace and inspiration for many. From the majestic mountains to the serene beaches, nature offers a retreat from the hustle and bustle of everyday life..."
         }
     }'
{"pathId":"/document/v1/doc/doc/docid/blog-1","id":"id:doc:doc::blog-1"}
$ curl -X POST "http://localhost:8080/document/v1/fess/doc/docid/blog-2" \
     -H "Content-Type: application/json" \
     -d '{
         "fields": {
             "doc_id": "blog-2",
             "title": "The Journey of Personal Growth",
             "content": "Personal growth is a continuous journey. It involves understanding oneself, setting meaningful goals, and pushing for constant improvement. While the path may be challenging, the rewards are truly significant."
         }
     }'
{"pathId":"/document/v1/doc/doc/docid/blog-2","id":"id:doc:doc::blog-2"}
$ curl -X POST "http://localhost:8080/document/v1/fess/doc/docid/blog-3" \
     -H "Content-Type: application/json" \
     -d '{
         "fields": {
             "doc_id": "blog-3",
             "title": "The Future of Technology and Innovation",
             "content": "The rapid pace of technological advancement is shaping our future. From artificial intelligence to renewable energy solutions, innovative ideas are at the forefront of creating a sustainable and interconnected world..."
         }
     }'
{"pathId":"/document/v1/fess/doc/docid/blog-1","id":"id:fess:doc::blog-1"}

id:doc:doc::blog-1のような感じのIDでそれぞれ登録されます。/document/v1のAPIは、/document/v1/[namespace]/[document type]/… のような形式で、今回はnamespaceはfessにして、document typeはdoc.sdでdocとしているので、docになります。namespaceは任意だと思うので、今回はfessにしてます(そのうち、FessのインデックスをVespaに入れてみたいなぁという希望を込めて…)。

IDで登録したドキュメントを取得してみます。

$ curl -X GET "http://localhost:8080/document/v1/fess/doc/docid/blog-1"|jq .
{
  "pathId": "/document/v1/fess/doc/docid/blog-1",
  "id": "id:fess:doc::blog-1",
  "fields": {
    "doc_id": "blog-1",
    "content": "Nature has always been a source of solace and inspiration for many. From the majestic mountains to the serene beaches, nature offers a retreat from the hustle and bustle of everyday life...",
    "title": "Exploring the Beauty of Nature"
  }
}

次に、全件取得的なリクエストはVisitという感じであるので、それを使ってみます。ElasticsearchでいうところのScrollみたいな感じです。

$ curl http://localhost:8080/document/v1/fess/doc/docid|jq .
{
  "pathId": "/document/v1/fess/doc/docid",
  "documents": [
    {
      "id": "id:fess:doc::blog-3",
      "fields": {
        "doc_id": "blog-3",
        "content": "The rapid pace of technological advancement is shaping our future. From artificial intelligence to renewable energy solutions, innovative ideas are at the forefront of creating a sustainable and interconnected world...",
        "title": "The Future of Technology and Innovation"
      }
    }
  ],
  "documentCount": 1,
  "continuation": "AAAACAAAAAAAAAAUAAAAAAAAABMAAAAAAAABAAAAAAEgAAAAAAAAyAAAAAAAAAAA"
}

という感じで、continuationが返ってくるので、これをリクエストパラメーターに追加して、次のドキュメントを取得します。

$ curl "http://localhost:8080/document/v1/fess/doc/docid?continuation=AAAACAAAAAAAAAAUAAAAAAAAABMAAAAAAAABAAAAAAEgAAAAAAAAyAAAAAAAAAAA"|jq .
{
  "pathId": "/document/v1/fess/doc/docid",
  "documents": [
    {
      "id": "id:fess:doc::blog-1",
      "fields": {
        "doc_id": "blog-1",
        "content": "Nature has always been a source of solace and inspiration for many. From the majestic mountains to the serene beaches, nature offers a retreat from the hustle and bustle of everyday life...",
        "title": "Exploring the Beauty of Nature"
      }
    }
  ],
  "documentCount": 1,
  "continuation": "AAAACAAAAAAAAABEAAAAAAAAAEMAAAAAAAABAAAAAAEgAAAAAAAAwgAAAAAAAAAA"
}
$ curl "http://localhost:8080/document/v1/fess/doc/docid?continuation=AAAACAAAAAAAAABEAAAAAAAAAEMAAAAAAAABAAAAAAEgAAAAAAAAwgAAAAAAAAAA"|jq .
{
  "pathId": "/document/v1/fess/doc/docid",
  "documents": [
    {
      "id": "id:fess:doc::blog-2",
      "fields": {
        "doc_id": "blog-2",
        "content": "Personal growth is a continuous journey. It involves understanding oneself, setting meaningful goals, and pushing for constant improvement. While the path may be challenging, the rewards are truly significant.",
        "title": "The Journey of Personal Growth"
      }
    }
  ],
  "documentCount": 1,
  "continuation": "AAAACAAAAAAAAACtAAAAAAAAAKwAAAAAAAABAAAAAAEgAAAAAAAANQAAAAAAAAAA"
}
$ curl "http://localhost:8080/document/v1/fess/doc/docid?continuation=AAAACAAAAAAAAACtAAAAAAAAAKwAAAAAAAABAAAAAAEgAAAAAAAANQAAAAAAAAAA"|jq .
{
  "pathId": "/document/v1/fess/doc/docid",
  "documents": [],
  "documentCount": 0
}

という感じで、全件取得したら終了します。

続いて、検索をしてみましょう。titleにjourneyが含まれるものを検索します。YQLで記述します。

$ curl -X POST "http://localhost:8080/search/" -H "Content-Type: application/json" -d '{
           "yql": "select * from sources doc where title contains \"journey\";"
         }'|jq .
{
  "root": {
    "id": "toplevel",
    "relevance": 1,
    "fields": {
      "totalCount": 1
    },
    "coverage": {
      "coverage": 100,
      "documents": 3,
      "full": true,
      "nodes": 1,
      "results": 1,
      "resultsFull": 1
    },
    "children": [
      {
        "id": "id:fess:doc::blog-2",
        "relevance": 0.16343879032006284,
        "source": "mind",
        "fields": {
          "sddocname": "doc",
          "documentid": "id:fess:doc::blog-2",
          "doc_id": "blog-2",
          "title": "The Journey of Personal Growth",
          "content": "Personal growth is a continuous journey. It involves understanding oneself, setting meaningful goals, and pushing for constant improvement. While the path may be challenging, the rewards are truly significant."
        }
      }
    ]
  }
}

ドキュメント更新は、以下のようにblog-2のtitleを更新します。

$ curl -X PUT "http://localhost:8080/document/v1/fess/doc/docid/blog-2" \
     -H "Content-Type: application/json" \
     -d '{
           "fields": {
             "title": {
               "assign": "The Trip of Personal Growth"
             }
           }
         }'
{"pathId":"/document/v1/fess/doc/docid/blog-2","id":"id:fess:doc::blog-2"}

あとは、最後に削除を試してみましょう。blog-1を削除します。

$ curl -X DELETE "http://localhost:8080/document/v1/fess/doc/docid/blog-1"
{"pathId":"/document/v1/fess/doc/docid/blog-1","id":"id:fess:doc::blog-1"}

という感じで、基本的なものを一通り試してみました。

最後にVespaを停止するのに

$ docker stop vespa

をして、終了です。

次は、rank-profileをいろいろと試したいところですね。

Vespaにチャレンジ

Vespa(ベスパ)について、紹介していこうと思います。

Vespaは高機能な検索エンジンです。ベクトル検索(ANN)、語彙検索、構造化データの検索が一つのクエリで可能です。さらに、統合された機械学習の推論を利用することで、データをリアルタイムで分析し、AIでの解析もできます。大規模なデータも扱えるので、様々なプロジェクトで利用できると思います。

Vespa自体は、歴史がある検索エンジンで、2017年にApacheライセンスでオープンソースとして、公開されています。過去には、Yahooのサービスなどでも利用されていたりもしたので、実績もあると思います。最近では、Vespa自体がスピンアウトして、会社になったようなので、普及に向けて、やる気を出してきた感じがあります。その会社では、クラウドサービスであるVespa Cloudを提供していく感じだと思います。

とはいえ、現状、普及している感じがない気がしてます…。ちょっと理由を考えていくと、

そもそもVespaという単語をググると、ほぼバイクしか、ヒットしないので、ググって情報を探すというのも困難です。あとは、公式サイトのドキュメントは充実している感じはあるのですが、欲しい情報を得るまでにいろいろと見ないとよくわからない、という学習コストの高さがあるようにも思います(個人的な見解ですが…)。

他にもrank-profile的なスキーマを書かないといけないところも難易度を上げている気もします。Elasticsearchでいうなら、マッピングを定義した後に、独自のSimilarityも定義しないといけないみたいな…。

それら以外にも、Yahooが提供している日本語の情報もあったりするのですが、古いので、手順通りにはできないので、雰囲気を掴むくらいにしか使えなかったりと…。欲しい情報にたどり着くのに苦労する感が…。

という感じの壁を乗り越えないと、使えない感じだと思います。ですが、他にはない興味深い機能があったりと、製品としては魅了があるものだと思います。

とりあえず、この敷居を下げるために(&自分自身の学習のために)、いろいろと情報を提供していければと思います。

まずは、簡単に使ってみましょう。Vespaを試すためには、

  • Docker (4GB以上のメモリーが利用可能であること)
  • vespaコマンド

があれば、試すことができます。詳しくはQuick Startとかを参照してください。

まず、vespaコマンドをインストールします。Macであれば、brewコマンドでインストールできます。

$ brew install vespa-cli

Linuxとかであれば、リリースサイトからVespa CLIをダンロードして、vespaコマンドにパスを通せばよいです。

では、早速、Dockerで起動しましょう。

$ docker run --detach --name vespa --hostname vespa-container \
--publish 8080:8080 --publish 19071:19071 \
vespaengine/vespa

8080ポートが検索やインデクシングするときに利用するもので、19071ポートはConfigサーバーのエンドポイントになります。

Vespaを利用するにはアプリをデプロイする必要があります。今回は、vespaコマンドでサンプルアプリを作成&デプロイします。

以下のコマンドを実行して、myappディレクトリにアプリが作られます。

$ vespa clone album-recommendation myapp && cd myapp

いくつかファイルが作成されますが、schemas/music.sdがスキーマ定義で、services.xmlがサービスの設定になります。この2つがわかれば、何とかなります。

今回は、さくっと試すだけなので、それらの中身の話は置いておいて、デプロイしてしまいましょう。

$ vespa deploy --wait 300

とすると、デプロイされます。

デプロイされたら、ext/documents.jsonlにあるドキュメントをフィードしましょう(データをインデクシングしましょう)。

$ vespa feed ext/documents.jsonl
{
  "feeder.seconds": 0.230,
  "feeder.ok.count": 5,
  "feeder.ok.rate": 5.000,
  "feeder.error.count": 0,
  "feeder.inflight.count": 0,
  "http.request.count": 5,
  "http.request.bytes": 724,
  "http.request.MBps": 0.001,
  "http.exception.count": 0,
  "http.response.count": 5,
  "http.response.bytes": 658,
  "http.response.MBps": 0.001,
  "http.response.error.count": 0,
  "http.response.latency.millis.min": 227,
  "http.response.latency.millis.avg": 227,
  "http.response.latency.millis.max": 227,
  "http.response.code.counts": {
    "200": 5
  }
}

フィードすると、情報が出力されます。”feeder.ok.count”が5なので、5件登録しました。

検索はYQLを利用して、たとえば、以下の感じで検索できます。albumにheadが含まれるもの的な。

$ vespa query "select * from music where album contains 'head'" \
language=en-US
{
    "root": {
        "id": "toplevel",
        "relevance": 1.0,
        "fields": {
            "totalCount": 1
        },
        "coverage": {
            "coverage": 100,
            "documents": 5,
            "full": true,
            "nodes": 1,
            "results": 1,
            "resultsFull": 1
        },
        "children": [
            {
                "id": "id:mynamespace:music::a-head-full-of-dreams",
                "relevance": 0.16343879032006287,
                "source": "music",
                "fields": {
                    "sddocname": "music",
                    "documentid": "id:mynamespace:music::a-head-full-of-dreams",
                    "artist": "Coldplay",
                    "album": "A Head Full of Dreams",
                    "year": 2015,
                    "category_scores": {
                        "type": "tensor<float>(cat{})",
                        "cells": {
                            "pop": 1.0,
                            "rock": 0.20000000298023224,
                            "jazz": 0.0
                        }
                    }
                }
            }
        ]
    }
}

あとは、ID指定で取得もできます。

$ vespa document get id:mynamespace:music::a-head-full-of-dreams
{
    "pathId": "/document/v1/mynamespace/music/docid/a-head-full-of-dreams",
    "id": "id:mynamespace:music::a-head-full-of-dreams",
    "fields": {
        "artist": "Coldplay",
        "year": 2015,
        "category_scores": {
            "type": "tensor<float>(cat{})",
            "cells": {
                "pop": 1.0,
                "rock": 0.20000000298023224,
                "jazz": 0.0
            }
        },
        "album": "A Head Full of Dreams"
    }
}

今回は、この辺にして、Dockerでstopすれば終了します。

$ docker stop vespa

以上の感じで、DockerとVespa CLIがあれば簡単に試すことができます。

今後もこんな感じで、いろいろと紹介できれば良いなと思います。