Skip to content

Search API

locallore.search

MAX_RESULTS = 25 module-attribute

MAX_CONTEXT = 10 module-attribute

EXCERPT_LENGTH = 500 module-attribute

RRF_K = 60 module-attribute

SearchResult

Bases: TypedDict

session_id instance-attribute

message_id instance-attribute

project instance-attribute

timestamp instance-attribute

role instance-attribute

score instance-attribute

excerpt instance-attribute

files instance-attribute

IndexStatus

Bases: TypedDict

last_refresh instance-attribute

refresh_errors instance-attribute

SearchResponse

Bases: TypedDict

results instance-attribute

index instance-attribute

ContextMessage

Bases: TypedDict

message_id instance-attribute

role instance-attribute

timestamp instance-attribute

text instance-attribute

ContextResponse

Bases: TypedDict

session_id instance-attribute

selected_message_id instance-attribute

messages instance-attribute

search_messages(connection, query, *, embedder=None, project=None, after=None, before=None, role=None, files=None, limit=8)

Source code in src/locallore/search.py
def search_messages(
    connection: sqlite3.Connection,
    query: str,
    *,
    embedder: Embedder | None = None,
    project: str | None = None,
    after: str | None = None,
    before: str | None = None,
    role: str | None = None,
    files: list[str] | None = None,
    limit: int = 8,
) -> SearchResponse:
    if role is not None and role not in {"user", "assistant", "tool"}:
        raise ValueError("role must be user, assistant, or tool")
    limit = max(1, min(limit, MAX_RESULTS))
    if not query.strip():
        raise ValueError("query must not be empty")
    if embedder is None and not re.search(r"\w+", query, flags=re.UNICODE):
        raise ValueError("query must contain at least one searchable term")
    filters, parameters = _filters(
        project=project, after=after, before=before, role=role, files=files
    )
    candidate_limit = limit * 4
    keyword_ranking = _keyword_ranking(
        connection, query, filters, parameters, candidate_limit
    )
    semantic_ranking = (
        _semantic_ranking(
            connection, query, embedder, filters, parameters, candidate_limit
        )
        if embedder is not None
        else []
    )
    fused = _fuse_rankings(keyword_ranking, semantic_ranking)
    if not fused:
        rows = []
    else:
        placeholders = ",".join("?" for _ in fused)
        fetched = connection.execute(
            "SELECT m.id, m.session_id, s.project, m.timestamp, m.role, m.text "
            "FROM messages m JOIN sessions s ON s.id = m.session_id "
            f"WHERE m.id IN ({placeholders})",
            [message_id for message_id, _ in fused],
        ).fetchall()
        by_id = {row["id"]: row for row in fetched}
        rows = [(by_id[message_id], score) for message_id, score in fused]
    results: list[SearchResult] = []
    seen_text: set[str] = set()
    for row, score in rows:
        normalized_text = " ".join(row["text"].split()).casefold()
        if normalized_text in seen_text:
            continue
        seen_text.add(normalized_text)
        paths = [
            item[0]
            for item in connection.execute(
                "SELECT path FROM file_operations WHERE message_id = ? ORDER BY path",
                (row["id"],),
            )
        ]
        results.append(
            {
                "session_id": row["session_id"],
                "message_id": row["id"],
                "project": row["project"],
                "timestamp": row["timestamp"],
                "role": row["role"],
                "score": round(score, 6),
                "excerpt": row["text"][:EXCERPT_LENGTH],
                "files": paths,
            }
        )
        if len(results) == limit:
            break
    refresh = connection.execute(
        "SELECT max(updated_at), count(*) FILTER (WHERE last_error IS NOT NULL) FROM import_files"
    ).fetchone()
    return {
        "results": results,
        "index": {"last_refresh": refresh[0], "refresh_errors": refresh[1]},
    }

get_context(connection, session_id, message_id, *, before=3, after=3)

Source code in src/locallore/search.py
def get_context(
    connection: sqlite3.Connection,
    session_id: str,
    message_id: str,
    *,
    before: int = 3,
    after: int = 3,
) -> ContextResponse:
    before = max(0, min(before, MAX_CONTEXT))
    after = max(0, min(after, MAX_CONTEXT))
    target = connection.execute(
        "SELECT source_line FROM messages WHERE id = ? AND session_id = ?",
        (message_id, session_id),
    ).fetchone()
    if target is None:
        raise ValueError("message was not found in the requested session")
    rows = connection.execute(
        "SELECT id, role, timestamp, text, source_line FROM messages "
        "WHERE session_id = ? AND source_line BETWEEN ? AND ? ORDER BY source_line",
        (session_id, target[0] - before, target[0] + after),
    ).fetchall()
    return {
        "session_id": session_id,
        "selected_message_id": message_id,
        "messages": [
            {
                "message_id": row["id"],
                "role": row["role"],
                "timestamp": row["timestamp"],
                "text": row["text"][:EXCERPT_LENGTH],
            }
            for row in rows
        ],
    }