# `Arcana.SearchResult`
[🔗](https://github.com/georgeguimaraes/arcana/blob/main/lib/arcana/search_result.ex#L1)

Normalized search result returned by `Arcana.search/2` for every mode.

All modes (`:vector`, `:keyword`, `:hybrid`, and graph-enhanced search)
return the same struct, so integrators never need per-mode formatters.

## Fields

  * `:id` - Chunk id (UUID string)
  * `:text` - Chunk text
  * `:document_id` - Owning document id (UUID string)
  * `:chunk_index` - Position of the chunk within its document
  * `:score` - Final relevance score for the mode that produced it
  * `:vector_score` - Vector similarity component. Only set by the
    single-query hybrid search on the pgvector backend, `nil` elsewhere.
  * `:keyword_score` - Keyword relevance component. Same availability
    as `:vector_score`.
  * `:rerank_score` - Score attached by rerankers that produce one
    (e.g. `Arcana.Reranker.ColBERT`), `nil` otherwise.
  * `:metadata` - The chunk's stored metadata with string keys.

The struct implements the `Access` behaviour for reads, so `result[:text]`
keeps working for code that used the previous plain maps.

## Serializing

Use `to_map/1`:

    results |> Enum.map(&Arcana.SearchResult.to_map/1) |> JSON.encode!()

The struct is deliberately not derived for `Jason.Encoder` or
`JSON.Encoder`. Deriving would make the field set part of the wire
contract, so adding or renaming a field would break everyone serializing
results. `to_map/1` is the seam instead: it returns a shape this module
commits to, and lets fields be added without exposing them.

Two things not to do. `Map.from_struct/1` works today but makes the
private field set your contract, so a field added later that isn't
JSON-safe breaks you at encode time. And a `defimpl JSON.Encoder, for:
Arcana.SearchResult` in your own application is an orphan implementation
for a struct you don't own: it collides if Arcana ever ships one, and with
any other library that did the same.

# `t`

```elixir
@type t() :: %Arcana.SearchResult{
  chunk_index: non_neg_integer() | nil,
  document_id: String.t() | nil,
  id: String.t() | term(),
  keyword_score: number() | nil,
  metadata: map(),
  rerank_score: number() | nil,
  score: number(),
  text: String.t(),
  vector_score: number() | nil
}
```

# `from_store_result`

Builds a result from a vector-store backend map (`%{id, score, metadata}`).

Backend metadata mixes the chunk's stored metadata with well-known keys
(`:text`, `:chunk_index`, `:document_id`, and hybrid score components);
the well-known keys become struct fields and the rest is kept under
`:metadata` with keys normalized to strings.

# `to_map`

```elixir
@spec to_map(t()) :: map()
```

The result as a plain map, for encoding, logging, or handing to a model.

Fields are listed explicitly rather than taken from the struct, so this
stays a contract: a field added to the struct later is not silently
exposed to everything that serializes a result, and the keys here can
outlive an internal rename.

    iex> result = %Arcana.SearchResult{id: "abc", text: "hello", score: 0.9}
    iex> map = Arcana.SearchResult.to_map(result)
    iex> {map.id, map.text, map.score, map.metadata}
    {"abc", "hello", 0.9, %{}}

`:metadata` is passed through as stored, with string keys.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
