77 lines
5.4 KiB
Markdown
77 lines
5.4 KiB
Markdown
Проект: /root/WORK/memo (Kotlin/JVM). Заказ по поиску.
|
||
|
||
Создать РОВНО ДВА новых файла:
|
||
memo-core/src/main/kotlin/memo/core/Searcher.kt
|
||
memo-core/src/test/kotlin/memo/core/SearcherTest.kt
|
||
|
||
## Searcher.kt
|
||
|
||
package memo.core
|
||
|
||
enum class SearchMode { HYBRID, LEX, VEC }
|
||
|
||
data class Hit(
|
||
val path: String,
|
||
val line: Int,
|
||
val heading: String,
|
||
val score: Double,
|
||
val text: String, // текст чанка, обрезанный до 1200 символов
|
||
)
|
||
|
||
fun interface RefreshHook { fun refresh(root: java.io.File) }
|
||
|
||
class Searcher(
|
||
private val db: Db,
|
||
private val embedder: Embedder,
|
||
private val refresh: RefreshHook? = null,
|
||
) {
|
||
/** Ищет по индексу. Если задан [refresh] — сначала вызывает его для root
|
||
* (догон свежих правок файлов до поиска). */
|
||
fun search(root: java.io.File, query: String, k: Int = 8, mode: SearchMode = SearchMode.HYBRID): List<Hit>
|
||
}
|
||
|
||
Правила реализации:
|
||
1. Если refresh != null — вызвать refresh.refresh(root) ПЕРЕД любым запросом к базе.
|
||
2. Лексический список (режимы LEX и HYBRID): разбить query на токены по всем символам, кроме букв
|
||
и цифр (unicode: Character.isLetterOrDigit); оставить токены длиной >= 2; взять первые 8;
|
||
собрать строку вида "ток1" OR "ток2" (каждый токен в двойных кавычках);
|
||
если токенов нет — лексический список пуст, без ошибки.
|
||
Запрос: SELECT rowid, bm25(chunks_fts) AS s FROM chunks_fts WHERE chunks_fts MATCH ? ORDER BY s LIMIT 32.
|
||
Ошибку SQLite на MATCH не ронять наружу, а считать списком пустым.
|
||
3. Векторный список (режимы VEC и HYBRID): embedder.embed(query) → FloatArray[768];
|
||
запрос: SELECT rowid, distance FROM chunks_vec WHERE embedding MATCH ? ORDER BY distance LIMIT 32.
|
||
Байндить вектор тем же способом, что в тесте vec0KnnRoundTrip (bindVector).
|
||
4. Слияние RRF: score(rowid) = сумма по спискам 1.0/(60.0 + rank), где rank — позиция в списке, начиная с 1.
|
||
В режиме LEX — только лексический список, в VEC — только векторный, в HYBRID — оба.
|
||
5. По итоговому score взять первые k rowid; вычитать из chunks поля path, line, heading, text
|
||
(SELECT path, line, heading, text FROM chunks WHERE id IN (...)); сохранить порядок по score убыванию.
|
||
6. text обрезать до 1200 символов (если длиннее — substring(0,1200) без многоточий).
|
||
7. Пустой индекс → пустой список, без исключений.
|
||
|
||
## SearcherTest.kt — ровно 5 тестов, имена ровно такие
|
||
|
||
Подготовка (общий помощник): временный каталог + Db(index.db) + db.init() + Embedder(модель из MEMO_MODEL_DIR
|
||
как в CoreSmokeTest.embedderProduces768). Помощник `addChunk(path, heading, line, text)`, который вставляет
|
||
строку в chunks, берёт last_insert_rowid() и вставляет тот же rowid в chunks_fts(text, heading)
|
||
и в chunks_vec(embedding) с эмбеддингом embedder.embed(text).
|
||
|
||
1. `lexModeFindsExactValue` — 3 чанка, в одном текст "Прокси корпоративных доменов на 76.132";
|
||
поиск с mode = LEX по запросу "76.132" → первый хит содержит "76.132".
|
||
2. `vecModeFindsSemanticMatch` — 3 чанка на разные темы (сервер/книга/CI);
|
||
запрос про «настройку выпуска приложения», mode = VEC → первый хит — чанк про CI.
|
||
3. `lexModeIgnoresVectorOnlyMatch` — запрос, не имеющий ни одного общего токена с чанками
|
||
(например, при тексте чанков "опрос", "случай", "знание" запрос "автомобиль");
|
||
mode = LEX → пустой список, mode = VEC → непустой.
|
||
4. `hybridCombinesBoth` — в индексе чанк с точным значением и чанк со смысловым совпадением;
|
||
запрос, попадающий в оба, mode = HYBRID, k = 5 → в результате присутствуют оба чанка.
|
||
5. `resultsRespectKAndTextLength` — 4 чанка; k = 2 → ровно 2 хита; у каждого text непустой и его длина <= 1200.
|
||
|
||
После: ./gradlew :memo-core:test --rerun-tasks — все тесты зелёные.
|
||
Коммит: git add -A && git commit -m "core: гибридный поиск (BM25 + вектор + RRF)"
|
||
|
||
СТРОГИЕ ЗАПРЕТЫ:
|
||
- Не выводить план текстом; сразу создавай файлы.
|
||
- Не трогать существующие файлы (Chunker.kt, Indexer.kt, Embedder.kt, Db.kt, CoreSmokeTest.kt, ChunkerTest.kt, build.gradle.kts).
|
||
- Не добавлять зависимости.
|
||
- Не создавать другие файлы.
|