Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

SearXNG

livebooks/ollama/searxng.livemd

SearXNG

Mix.install([
  {:req, "~> 0.5"},
  {:ollama, "~> 0.8"},
  {:chunx, github: "preciz/chunx"},
  {:hnswlib, "~> 0.1"},
  {:kino, "~> 0.15"}
])

Search

search = fn query ->
  "http://searxng:8080/search"
  |> Req.get!(params: [q: query, format: "json"])
  |> Map.get(:body)
end
results =
  "やせうま"
  |> search.()
  |> Map.get("results")
documents =
  results
  |> Enum.filter(fn result ->
    String.starts_with?(result["url"], "https")
  end)
  |> Enum.slice(0..9)
  |> Enum.map(fn result ->
    content =
      result["url"]
      |> URI.encode()
      |> Req.get!()
      |> Map.get(:body)
      |> String.replace(~r/\n/, "🦛")
      |> String.replace(~r//, "")
      |> String.replace(~r//, "")
      |> String.replace(~r/[\s]+/, "")
      |> String.replace(" ", " ")
      |> String.replace(~r//, "\n")
      |> String.replace(~r/<.*?>/, "\t")
      |> String.replace("🦛", "\n")
      |> String.replace(~r/[\r\n\t]+/, "\n")
      |> String.trim()

    %{
      url: result["url"],
      title: result["title"],
      content: content
    }
  end)
  |> Enum.filter(fn result ->
    String.valid?(result.content)
  end)
  |> Enum.slice(0..4)

Chunk

{:ok, tokenizer} = Tokenizers.Tokenizer.from_file("/tmp/ruri_base/tokenizer.json")
client = Ollama.init(base_url: "http://host.docker.internal:11434/api", receive_timeout: 300_000)
Ollama.pull_model(client, name: "kun432/cl-nagoya-ruri-base")
embedding_fn = fn texts ->
  texts
  |> Enum.map(fn text ->
    client
    |> Ollama.embed(
      model: "kun432/cl-nagoya-ruri-base",
      input: "文章: #{text}"
    )
    |> elem(1)
    |> Map.get("embeddings")
    |> hd()
    |> Nx.tensor()
  end)
end
chunks =
  documents
  |> Enum.map(fn document ->
    {:ok, doc_chunks} =
      document.content
      |> Chunx.Chunker.Semantic.chunk(
        tokenizer,
        embedding_fn,
        delimiters: ["。", ".", "!", "?", "\n"]
      )

    doc_chunks
    |> Enum.map(fn chunk ->
      chunk.sentences
      |> Enum.filter(fn sentence -> String.length(sentence.text) > 2 end)
      |> Enum.map(fn sentence ->
        document
        |> Map.put(:chunk, chunk.text)
        |> Map.put(:sentence, sentence.text)
        |> Map.put(:embedding, sentence.embedding)
        |> Map.delete(:content)
      end)
    end)
    |> Enum.concat()
  end)
  |> Enum.concat()

Index

{:ok, index} = HNSWLib.Index.new(:cosine, 768, 1_000_000)

chunks
|> Enum.each(fn chunk ->
  HNSWLib.Index.add_items(index, chunk.embedding)
end)
query = "やせうまの材料は何ですか"

embeddings =
  client
  |> Ollama.embed(
    model: "kun432/cl-nagoya-ruri-base",
    input: "クエリ: #{query}"
  )
  |> elem(1)
  |> Map.get("embeddings")
  |> hd()
  |> Nx.tensor()

{:ok, labels, dist} = HNSWLib.Index.knn_query(index, embeddings, k: 10)
context =
  labels
  |> Nx.to_flat_list()
  |> Enum.map(fn index ->
    chunks
    |> Enum.at(index)
    |> Map.get(:chunk)
  end)
  |> Enum.join("\n\n")

Kino.Markdown.new(context)

Chat

Ollama.pull_model(client, name: "hf.co/alfredplpl/gemma-2-2b-jpn-it-gguf")
Ollama.preload(client, model: "hf.co/alfredplpl/gemma-2-2b-jpn-it-gguf")
{:ok, %{"response" => response}} =
  Ollama.completion(
    client,
    model: "hf.co/alfredplpl/gemma-2-2b-jpn-it-gguf",
    prompt: """
    質問に一般的な情報ではなく、コンテキスト情報のみに基づいて回答してください
  
    ## コンテキスト情報
  
    #{context}
  
    ## 質問
  
    #{query}
    """
  )

Kino.Markdown.new(response)