Files
memo/docs/orders/04-searcher.md
T

77 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Проект: /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).
- Не добавлять зависимости.
- Не создавать другие файлы.