Packages
fnord
0.8.77
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/search/conversations.ex
defmodule Search.Conversations do
@moduledoc """
Semantic search over indexed conversations.
This module uses conversation embeddings stored via
`Store.Project.ConversationIndex` to find relevant conversations for a
natural language query.
"""
alias Store.Project
alias Store.Project.Conversation
alias Store.Project.ConversationIndex
@default_limit 5
@spec search(Project.t(), String.t(), keyword()) :: {:ok, [map()]} | {:error, term()}
def search(%Project{} = project, query, opts \\ []) when is_binary(query) do
limit = Keyword.get(opts, :limit, @default_limit)
with {:ok, query_vec} <- Indexer.impl().get_embeddings(query) do
project
|> ConversationIndex.all_embeddings()
|> Util.async_stream(fn {id, emb_vec, _meta} ->
score = AI.Util.cosine_similarity(query_vec, emb_vec)
build_result(project, id, score)
end)
|> Enum.reduce([], fn
{:ok, nil}, acc -> acc
{:ok, result}, acc -> [result | acc]
end)
|> Enum.sort_by(fn %{score: sc} -> sc end, :desc)
|> Enum.take(limit)
|> Enum.sort_by(
fn %{timestamp: ts} ->
case ts do
%DateTime{} = dt -> DateTime.to_unix(dt)
ts when is_integer(ts) -> ts
_ -> 0
end
end,
:desc
)
|> then(&{:ok, &1})
end
end
defp build_result(project, id, score) do
convo = Conversation.new(id, project)
if Conversation.exists?(convo) do
ts = Conversation.timestamp(convo)
title = unwrap_question(Conversation.question(convo))
length = Conversation.num_messages(convo)
%{
conversation_id: id,
title: title,
timestamp: ts,
length: length,
score: score
}
else
nil
end
end
defp unwrap_question({:ok, q}), do: q
defp unwrap_question(_), do: "(no user question found)"
end