USearchに入門中の阿部です。
入門することにした経緯などはUSearchに入門する準備をご覧ください。
ということで、前回に入門する準備をしたので今回は入門したいと思います。
記事に登場するコード例はmain-dev
ブランチの113a7862f80bf2eb347c559da8487c4be05a5cc4
時点のコードを利用しています。
前回のふりかえりと今回やること
前回はUSearchをビルドして、JavaScriptの簡単なコード例を実行しました。
今回はUSearchをGroongaからいい感じに使えるかどうかを調査しつつ、USearchについて学んでいきます。
「USearchをGroongaからいい感じに使えるか」のひとつの観点としてファイルベース(mmap)で利用できるかどうか、があります。 Groongaはファイルベース(mmap)で読み書きするからです。 特にその点を調査していきます。
USearchがファイルベース(mmap)で使えるか
さっそく結論ですが、USearchはファイルベース(mmap)で「読み書き」できません。 書き込みについてはすべてRAM上で処理されます。
ファイルへ書き出したインデックス情報を読み込むことで、「読み込みのみ」のファイルベース(mmap)をサポートしていますが、Groongaは「読み書き」をファイルベース(mmap)で行いたいので、現状USearchはそのまま利用できません。
参考: Serialization & Serving Index from Disk
実演
インデックスの書き出しと読み込みについてJavaScriptで実演です。ドキュメントのコード例を元にいろいろ試します。
1. index.load()
を試す
index.save()
でインデックスをファイルに保存できます。
またindex.load()
で保存したインデックスの情報を読み込めます。
index.load()
はRAMに読み書き可能な状態でロードするのでadd()
などで更新することもできます。
const usearch = require('usearch');
const index = new usearch.Index({ metric: 'l2sq', connectivity: 16, dimensions: 3 });
index.add(42n, new Float32Array([0.2, 0.6, 0.4]));
console.log('Added 42n.');
console.log(index.search(new Float32Array([0.2, 0.6, 0.4]), 10).keys);
// indexをファイルに保存する
index.save('index.usearch');
// indexを追加する
index.add(43n, new Float32Array([0.3, 0.6, 0.4]));
console.log('Added 43n.');
console.log(index.search(new Float32Array([0.2, 0.6, 0.4]), 10).keys);
// さっき保存したindexをloadする
index.load('index.usearch');
console.log('Loaded.');
console.log(index.search(new Float32Array([0.2, 0.6, 0.4]), 10).keys);
// loadしたindexに追加
index.add(43n, new Float32Array([0.3, 0.6, 0.4]));
console.log('Added 43n after loading.');
console.log(index.search(new Float32Array([0.2, 0.6, 0.4]), 10).keys);
// もう一回loadする
index.load('index.usearch');
console.log('Load again.');
console.log(index.search(new Float32Array([0.2, 0.6, 0.4]), 10).keys);
// 実行結果:
// Added 42n.
// BigUint64Array(1) [ 42n ]
// Added 43n.
// BigUint64Array(2) [ 42n, 43n ]
// Loaded.
// BigUint64Array(1) [ 42n ]
// Added 43n after loading.
// BigUint64Array(2) [ 42n, 43n ]
// Load again.
// BigUint64Array(1) [ 42n ]
当たり前の話ではありますが、RAM上での処理されるのでロードしたファイルへ43n
が追加されていないことがわかります。
2. index.view()
を試す
index.load()
と同様にindex.view()
も試してみます。
const usearch = require('usearch');
const index = new usearch.Index({ metric: 'l2sq', connectivity: 16, dimensions: 3 });
index.add(42n, new Float32Array([0.2, 0.6, 0.4]));
console.log('Added 42n.');
console.log(index.search(new Float32Array([0.2, 0.6, 0.4]), 10).keys);
// indexをファイルに保存する
index.save('index.usearch');
// さっき保存したindexをviewする
index.view('index.usearch');
console.log('Viewed.');
console.log(index.search(new Float32Array([0.2, 0.6, 0.4]), 10).keys);
// viewしたindexに追加
try {
console.log('Add 43n after view.');
index.add(43n, new Float32Array([0.3, 0.6, 0.4]));
} catch (err) {
console.log(err.message);
}
// viewしたindexでremove
try {
console.log('Remove 42n after view.');
index.remove(42n);
} catch (err) {
console.log(err.message);
}
// 実行結果:
// Added 42n.
// BigUint64Array(1) [ 42n ]
// Viewed.
// BigUint64Array(1) [ 42n ]
// Add 43n after view.
// Can't add to an immutable index
// Remove 42n after view.
// Can't remove from an immutable index
実行結果からはわかりにくいですが、index.view()
で読み込むと、読み込みのみのmmapとしてインデックスがロードされます。
ですので、add()
やremove()
を実行するとエラーになります。
中締め
USearchはファイルベース(mmap)を読み込みしかサポートしていないことがわかった。
どうしてもファイルベース(mmap)で読み書きしたい!
ファイルベース(mmap)での読み書きをサポートしていないならば、サポートすれば良いじゃないか、ということでファイルベース(mmap)での読み書きをサポートするのがどのくらい大変そうか確認してみたいと思います。
暫定的にコードを変更してファイルベース(mmap)での読み書きが動きそうか試します。
雑に読み書きモードを試す
暫定的に試すだけなので、雑に「読み込みのみ」で開いているファイルを「読み書き」で開くように変更します。 Ubuntuで作業をしているので関係するところだけを修正します。
またview()
したインデックスに対してadd()
など更新系の処理を抑止するif
もあるのでそれもコメントアウトしておきます。
diff --git a/include/usearch/index.hpp b/include/usearch/index.hpp
index 13cd618..708b5b5 100644
--- a/include/usearch/index.hpp
+++ b/include/usearch/index.hpp
@@ -1794,7 +1794,7 @@ class memory_mapped_file_t {
#else
#if defined(USEARCH_DEFINED_LINUX)
- int descriptor = open(path_, O_RDONLY | O_NOATIME);
+ int descriptor = open(path_, O_RDWR);
#else
int descriptor = open(path_, O_RDONLY);
#endif
@@ -1810,7 +1810,8 @@ class memory_mapped_file_t {
}
// Map the entire file
- byte_t* file = (byte_t*)mmap(NULL, file_stat.st_size, PROT_READ, MAP_SHARED, descriptor, 0);
+ byte_t* file = (byte_t*)mmap(NULL, file_stat.st_size, PROT_WRITE, MAP_SHARED, descriptor, 0);
+
if (file == MAP_FAILED) {
::close(descriptor);
return result.failed(std::strerror(errno));
@@ -2711,8 +2712,8 @@ class index_gt {
prefetch_at&& prefetch = prefetch_at{}) usearch_noexcept_m {
add_result_t result;
- if (is_immutable())
- return result.failed("Can't add to an immutable index");
+ // if (is_immutable())
+ // return result.failed("Can't add to an immutable index");
// Make sure we have enough local memory to perform this request
context_t& context = contexts_[config.thread];
@@ -2848,7 +2849,7 @@ class index_gt {
if (!config.expansion)
config.expansion = default_expansion_add();
- usearch_assert_m(!is_immutable(), "Can't add to an immutable index");
+ // usearch_assert_m(!is_immutable(), "Can't add to an immutable index");
add_result_t result;
compressed_slot_t updated_slot = iterator.slot_;
diff --git a/include/usearch/index_dense.hpp b/include/usearch/index_dense.hpp
index 3a5ab31..a4b3034 100644
--- a/include/usearch/index_dense.hpp
+++ b/include/usearch/index_dense.hpp
@@ -1463,8 +1463,8 @@ class index_dense_gt {
labeling_result_t remove(vector_key_t key) {
usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled");
labeling_result_t result;
- if (typed_->is_immutable())
- return result.failed("Can't remove from an immutable index");
+ // if (typed_->is_immutable())
+ // return result.failed("Can't remove from an immutable index");
unique_lock_t lookup_lock(slot_lookup_mutex_);
auto matching_slots = slot_lookup_.equal_range(key_and_slot_t::any_slot(key));
うまくいってそう
読み書きモードに変更したUSearchをビルドして、JavaScriptで実行してみます。
const usearch = require('usearch');
const index = new usearch.Index({ metric: 'l2sq', connectivity: 16, dimensions: 3 });
index.add(41n, new Float32Array([0.1, 0.6, 0.4]));
index.add(42n, new Float32Array([0.2, 0.6, 0.4]));
console.log('Added 41n, 42n.');
console.log(index.search(new Float32Array([0.2, 0.6, 0.4]), 10).keys);
// indexをファイルに保存する
index.save('index.usearch');
// 保存したindexをviewする
index.view('index.usearch');
console.log('Viewed.');
console.log(index.search(new Float32Array([0.2, 0.6, 0.4]), 10).keys);
// removeする
index.remove(41n);
console.log('Removed 41.');
console.log(index.search(new Float32Array([0.2, 0.6, 0.4]), 10).keys);
// もう一回viewする
index.view('index.usearch');
console.log('View again.');
console.log(index.search(new Float32Array([0.2, 0.6, 0.4]), 10).keys);
// viewしたindexに追加
index.add(43n, new Float32Array([0.3, 0.6, 0.4]));
console.log('Added 43n after viewing.');
console.log(index.search(new Float32Array([0.2, 0.6, 0.4]), 10).keys);
// さらにもう一回viewする
index.view('index.usearch');
console.log('View again again.');
console.log(index.search(new Float32Array([0.2, 0.6, 0.4]), 10).keys);
// 実行結果:
// Added 41n, 42n.
// BigUint64Array(2) [ 42n, 41n ]
// Viewed.
// BigUint64Array(2) [ 42n, 41n ]
// Removed 41.
// BigUint64Array(1) [ 42n ]
// View again.
// BigUint64Array(1) [ 42n ]
// Added 43n after viewing.
// BigUint64Array(2) [ 42n, 43n ]
// View again again.
// BigUint64Array(2) [ 42n, 43n ]
上述のindex.load()
で試したときはファイルに保存したインデックスをロードし直すと、index.save()
した時点のインデックスデータに戻りました。
しかし今回はIndex.view()
でロードし直しても直前に行った処理がファイルにも反映されていることがわかります!やった!簡単じゃん!
簡単じゃなかった
JavaScriptのコードを変えて試したら、落ちました。そんな簡単な話ではなかった。
const usearch = require('usearch');
const index = new usearch.Index({ metric: 'l2sq', connectivity: 16, dimensions: 3 });
index.add(41n, new Float32Array([0.1, 0.6, 0.4]));
index.add(42n, new Float32Array([0.2, 0.6, 0.4]));
console.log('Added 41n, 42n.')
console.log(index.search(new Float32Array([0.2, 0.6, 0.4]), 10).keys);
// indexをファイルに保存する
index.save('index.usearch');
// 保存したindexをviewする
index.view('index.usearch');
console.log('Viewed.');
console.log(index.search(new Float32Array([0.2, 0.6, 0.4]), 10).keys);
// たくさん追加する
const keys = [];
const vectors = [];
for (let i = 100; i < 130; i++) {
keys.push(BigInt(i))
vectors.push(new Float32Array([0.2 * i, 0.6 * i, 0.4 * i]))
}
index.add(keys, vectors);
console.log('Added a lot.')
// もう一回viewする
index.view('index.usearch');
console.log('Viewed agin.');
console.log(index.search(new Float32Array([0.2, 0.6, 0.4]), 10).keys);
// 実行結果:
// Added 41n, 42n.
// BigUint64Array(2) [ 42n, 41n ]
// Viewed.
// BigUint64Array(2) [ 42n, 41n ]
// Added a lot.
// Viewed agin.
// Segmentation fault (core dumped)
まとめ
GroongaでUSearchを使う場合、USearchがファイルベース(mmap)の読み書きをサポートしている必要がありますが、現状ではサポートしていないことを確認しました。
また、サポートしていないならばサポートすれば良いじゃないか!ということで簡単に試してみたところ、実現できそうな雰囲気は感じられました。
ということで次回は Segmentation fault (core dumped)
の原因を解決して、USearchがファイルベース(mmap)で読み書きできるようにすることを目指します!