CLOVER🍀

That was when it all began.

ベクトル検索で使う類似度の比較方法について

これは、なにをしたくて書いたもの?

ベクトルデータベースというかベクトル検索を扱うと、検索で使う類似度の比較方法の指定を求められます。

どういう時になにを選んだらいいのか全然わからないのですが、少しヒントになりそうな情報をまとめておこうかなと。

ざっくりまとめ?

ベクトル検索でよく出てくる類似度の比較方法。

  • ユークリッド距離
    • 2つのベクトル間の直線距離を測定
    • 直線距離が少ないほど似ているということになる
  • コサイン類似度
    • 2つのベクトル間の角度のコサインを測定して判定
    • 同じ向き(0度)に近づくと似ている、90度のように直交すると無関係、180度に近づくほど似ていないと判定される
  • ドット積
    • 正規化されたベクトルに対して、2つのベクトル間の角度のコサインを測定して判定したもの
  • 最大内積
    • 2つのベクトルの内積をとる
    • 内積の値が大きければ似ているということになる

参考

5-11. レコメンドシステムではなぜユークリッド距離ではなくコサイン類似度が用いられるのか | Vignette & Clarity(ビネット&クラリティ)

コサイン類似度(Cosine Similarity)とは?:AI・機械学習の用語辞典 - @IT

Vector Search:4つのベクトル検索アルゴリズム:コサイン類似度、ユークリッド距離、マンハッタン距離、負の内部積(スカラー積)

ベクトルデータベースとは何ですか?|包括的なベクトルデータベースのガイド | Elastic

【院生が徹底解説】ChatGPTのベクトルデータベースとは? | WEEL

個人的には九州大学が公開している資料がわかりやすかったと思います。

講義資料 – 九州大学 数理・データサイエンス教育研究センター

データサイエンス概論I&II / データ間の距離と類似度

情報科学 【AI・データサイエンス 】 / 第5回 ベクトル・距離・類似度

ここの例えから先が特に。

また、その後で5-11. レコメンドシステムではなぜユークリッド距離ではなくコサイン類似度が用いられるのか | Vignette & Clarity(ビネット&クラリティ)にある画像と内容を見ると、よりイメージしやすかったです。

さて、ユーザーAは「PCパーツに興味があるヘビーユーザー」、ユーザーBは「PCパーツに興味があるライトユーザー」、ユーザーCは「Tシャツに興味があるライトユーザー」と解釈できます。

ここでユーザーBにレコメンドする商品は何が良いでしょうか?

とはいえ

各類似度の比較方法の考え方はざっくりわかったのですが、結局のところあるデータをベクトル化した際にどんなベクトルになるのか、
そしてそのベクトルはどういう考え方で比較するのが対象の文脈において適切なのかがわからないと、類似度の比較方法なんて選べないと
いうことですね。

それはそうですね、という感じではありますが…。

Apache Lucene 9.0.0でRAMDirectoryが削除されていたという話(代わりにByteBuffersDirectoryを使う)

これは、なにをしたくて書いたもの?

前に、こういうエントリーを書きました。

Apache Luceneでベクトル検索(kNN検索)を試す - CLOVER🍀

自分はApache Luceneで簡単なプログラムを書く時に、インデックスの保存先をインメモリーにすることが多いのですが、その用途で
いつも使っていたRAMDirectoryがなくなっていてちょっと困ったので。

今はByteBuffersDirectoryを使うのがよさそうです。

Apache Lucene 9.0.0でのRAMDirectoryの削除

RAMDirectoryが削除されたのは、Apache Lucene 9.0.0のようです。

Lucene Change Log / Release 9.0.0 / API Changes

対象のissue。

Placeholder for the remainder of the original patch, removing all 8.x-deprecated RAMDirectory classes and replacing their use cases with ByteBuffersDirectory.

[LUCENE-8474] Remove deprecated RAMDirectory - ASF JIRA

Apache Lucene 8.0.0の時点で、RAMDirectoryは非推奨になっていたようですね。

Deprecated.
This class uses inefficient synchronization and is discouraged in favor of MMapDirectory. It will be removed in future versions of Lucene.

RAMDirectory (Lucene 8.0.0 API)

同時実行性が低く、パフォーマンスが悪いことが理由のようです。

[LUCENE-8438] RAMDirectory speed improvements and cleanup - ASF JIRA

というわけで、置き換え先はByteBuffersDirectoryですね。

ByteBuffersDirectory (Lucene 9.10.0 core API)

名前のとおり、java.nio.ByteBufferを使ったApache LuceneのDirectoryの実装です。

ただこのクラス、実験的APIなんですけどね…。

WARNING: This API is experimental and might change in incompatible ways in the next release.

もっとも、これを使うのはテスト用途だったりすると思うので、特に問題ないでしょう。

今回はこちらを使ったコードを載せて終わりにしようと思います。

環境

今回の環境はこちら。

$ java --version
openjdk 21.0.2 2024-01-16
OpenJDK Runtime Environment (build 21.0.2+13-Ubuntu-122.04.1)
OpenJDK 64-Bit Server VM (build 21.0.2+13-Ubuntu-122.04.1, mixed mode, sharing)


$ mvn --version
Apache Maven 3.9.6 (bc0240f3c744dd6b6ec2920b3cd08dcc295161ae)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 21.0.2, vendor: Private Build, runtime: /usr/lib/jvm/java-21-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-107-generic", arch: "amd64", family: "unix"

準備

Maven依存関係など。

    <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>9.10.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-queryparser</artifactId>
            <version>9.10.0</version>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.25.3</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

Query Parserはなんとなく使った感じですね。

ByteBuffersDirectoryを使う

ByteBuffersDirectoryを使ったテストコードはこちら。

src/test/java/org/littlewings/lucene/directory/ByteBuffersDirectoryTest.java

package org.littlewings.lucene.directory;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.StoredFields;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.ByteBuffersDirectory;
import org.apache.lucene.store.Directory;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Arrays;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

class ByteBuffersDirectoryTest {
    @Test
    void inMemory() throws IOException, ParseException {
        try (Directory directory = new ByteBuffersDirectory()) {
            Analyzer analyzer = new StandardAnalyzer();
            IndexWriterConfig config = new IndexWriterConfig(analyzer);

            try (IndexWriter writer = new IndexWriter(directory, config)) {
                Document document1 = new Document();
                document1.add(new TextField("field1", "Apache Lucene", Field.Store.YES));
                writer.addDocument(document1);

                Document document2 = new Document();
                document2.add(new TextField("field1", "Elasticsearch", Field.Store.YES));
                writer.addDocument(document2);

                Document document3 = new Document();
                document3.add(new TextField("field1", "Apache Solr", Field.Store.YES));
                writer.addDocument(document3);
            }

            try (DirectoryReader reader = DirectoryReader.open(directory)) {
                IndexSearcher searcher = new IndexSearcher(reader);

                QueryParser queryParser = new QueryParser("field1", analyzer);
                Query query = queryParser.parse("field1: Apache");

                TopDocs topDocs =
                        searcher.search(query, 10);
                ScoreDoc[] scoreDocs = topDocs.scoreDocs;
                StoredFields storedFields = searcher.storedFields();

                List<Document> resultDocuments =
                        Arrays
                                .stream(scoreDocs)
                                .map(scoreDoc -> {
                                    try {
                                        return storedFields.document(scoreDoc.doc);
                                    } catch (IOException e) {
                                        throw new UncheckedIOException(e);
                                    }
                                })
                                .toList();

                assertThat(resultDocuments.get(0).getField("field1").stringValue()).isEqualTo("Apache Lucene");
                assertThat(resultDocuments.get(1).getField("field1").stringValue()).isEqualTo("Apache Solr");
            }
        }
    }
}

使い方はとても簡単で、インスタンスを作成してApache LuceneのDirectoryとして使えばOKです。

        try (Directory directory = new ByteBuffersDirectory()) {

おわりに

Apache Lucene 9.0.0より前で使えていたRAMDirectoryの代替になるのは、ByteBuffersDirectoryということを書きました。

話としてはそれだけなのですが、RAMDirectoryが見つからなかった時に「さて代わりは???」と探すのにちょっと困ったので
メモとして。