Skip to main content
Glama
fulltext_search.ex4.41 kB
defmodule HexdocsMcp.FulltextSearch do @moduledoc """ Functions for searching HexDocs using their full-text search API. """ @behaviour HexdocsMcp.Behaviours.FulltextSearch @search_api_base "https://search.hexdocs.pm" @doc """ Performs full-text search on HexDocs using Typesense. ## Options * `:package` - Optional package name to filter results * `:version` - Optional version to filter results (requires :package) * `:limit` - Maximum number of results to return. Defaults to 10, max 100. * `:type` - Optional document type filter (e.g., "function", "module", "type", "callback") * `:page` - Optional page number for pagination. Defaults to 1. """ def search(query, opts \\ []) do package = Keyword.get(opts, :package) version = Keyword.get(opts, :version) limit = min(Keyword.get(opts, :limit, 10), 100) type = Keyword.get(opts, :type) page = Keyword.get(opts, :page, 1) params = %{ "q" => query, "query_by" => "doc,title", "per_page" => limit, "page" => page, # Include more fields in response for better context "include_fields" => "title,doc,type,ref,package,proglang", # Highlight settings for better snippets "highlight_affix_num_tokens" => 10, "highlight_full_fields" => "doc" } # Build filter_by clause filters = [] filters = if package && version do ["package:=#{package}-#{version}" | filters] else if package do # For package-only filter, we need to match any version of that package ["package:#{package}-*" | filters] else filters end end filters = if type do ["type:=#{type}" | filters] else filters end params = if length(filters) > 0 do Map.put(params, "filter_by", Enum.join(filters, " && ")) else params end case Req.get(@search_api_base, params: params) do {:ok, %{status: 200, body: %{"hits" => hits, "found" => found} = body}} -> results = Enum.map(hits, &format_search_result/1) search_info = %{ total_found: found, page: page, per_page: limit, search_time_ms: Map.get(body, "search_time_ms", 0) } {:ok, results, search_info} {:ok, %{status: 200, body: %{"message" => message}}} -> {:error, "Search failed: #{message}"} {:ok, %{status: status_code, body: body}} -> {:error, "Failed to search HexDocs: HTTP #{status_code} - #{inspect(body)}"} {:error, reason} -> {:error, "HTTP request failed: #{inspect(reason)}"} end end defp format_search_result(%{"document" => doc, "highlight" => highlight} = result) do %{ package: doc["package"], ref: doc["ref"], title: doc["title"], type: doc["type"], proglang: Map.get(doc, "proglang", "elixir"), url: build_doc_url(doc), snippet: get_best_snippet(highlight), matched_tokens: get_matched_tokens(highlight), score: Map.get(result, "text_match", 0), highlights: format_highlights(Map.get(result, "highlights", [])) } end defp format_highlights(highlights) do Enum.map(highlights, fn h -> %{ field: h["field"], snippet: h["snippet"], matched_tokens: h["matched_tokens"] } end) end defp build_doc_url(%{"package" => package, "ref" => ref}) do # Extract package name and version from package field (format: "package-version") case String.split(package, "-") do [pkg_name | version_parts] when version_parts != [] -> version = Enum.join(version_parts, "-") "https://hexdocs.pm/#{pkg_name}/#{version}/#{ref}" _ -> "https://hexdocs.pm/#{package}/#{ref}" end end defp get_best_snippet(%{"doc" => %{"snippet" => snippet}}), do: snippet defp get_best_snippet(%{"title" => %{"snippet" => snippet}}), do: snippet defp get_best_snippet(_), do: "" defp get_matched_tokens(highlight) do tokens = [] tokens = if doc_tokens = get_in(highlight, ["doc", "matched_tokens"]) do tokens ++ doc_tokens else tokens end tokens = if title_tokens = get_in(highlight, ["title", "matched_tokens"]) do tokens ++ title_tokens else tokens end Enum.uniq(tokens) end end

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bradleygolden/hexdocs-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server