ANSWERS · FILES AND FORMATS

How do I run syntax-aware code search?

AUTHOR · XERJ documentation team · REVIEWED BY · XERJ engineering team · PUBLISHED · 2026-08-21 · UPDATED · 2026-10-30
SHORT ANSWER · UPDATED 2026-10-30

Is there a syntax-aware code search tool for refactoring? XERJ gives you two code queries on one index: match on body for raw text, and match on defs for declared symbols, which returns a symbols array with kind, name and line. It extracts definitions only and rewrites nothing. For structural rewrites use ast-grep.

Key number: XERJ's tree-sitter registry covers 36 languages, pinned by a unit test. (as of 2026-10-19 · source: engine/crates/xerj-autoindex/src/extract/code.rs)

TL;DR XERJ gives you 2 code queries on one index. A match on body finds raw text, and a match on defs finds declared symbols with a kind, a name and a line. Extraction is definitions only, so XERJ rewrites nothing.

Recorded capture: 3 files, 3 languages

One index, two questions

match on body

{"query":{"match":{"body":"checkpoint_journal_offset"}}}
ax_pathscore
src/ingest.py0.5359981
src/lib.rs0.4608517

Any occurrence in the file text, including a call inside a function body.

match on defs

{"query":{"match":{"defs":"merge_segments"}}}
hits2 filesEach hit means the file declares the name, and each carries a symbols array.
missedMergeSegments in src/query.goThe token merge_segments did not match it. Spell the name the way the language does.
Both queries from the capture on this page, run against one index on 1 single-node XERJ process. Scores are the _score values returned.
PASTE THIS TO YOUR AI AGENT — IT DOES THE REST Act as a coding agent. Read https://xerj.org/llms.txt, start a XERJ node with --insecure, run xerj autoindex ./code --prefix code, then send two queries to code-*/_search: a match on body for a symbol name and a match on defs for the same name, and report ax_path, language and the symbols array with kind, name and line for each declaration hit.
RUN THIS XERJ COMMAND xerj autoindex ./code --url http://127.0.0.1:9410 --prefix code --state-dir ./state-code --progress plain --disable-feedback Index the source tree into the prefix code.
RUN THIS XERJ COMMAND curl -s -XPOST 'http://127.0.0.1:9410/code-*/_search' -H 'content-type: application/json' -d '{"query":{"match":{"body":"checkpoint_journal_offset"}},"size":10,"_source":["ax_path","language","title","defs","symbol_count"],"track_total_hits":true}' Find every textual occurrence of a symbol name.
RUN THIS XERJ COMMAND curl -s -XPOST 'http://127.0.0.1:9410/code-*/_search' -H 'content-type: application/json' -d '{"query":{"match":{"defs":"merge_segments"}},"size":10,"_source":["ax_path","language","title","defs","symbols"],"track_total_hits":true}' Find only the files that declare the symbol.

One index, two fields

xerj autoindex writes body, defs, symbols, symbol_count, language and title for every source file. Query body for any occurrence. Query defs for a declaration. The first command above indexes a source tree into the prefix code, and the next two are the queries in the figure.

What the definition hit carries

A defs hit returns the file's symbols array. That array is what a refactoring step consumes: it says where each declaration starts.

Recorded capture: the Rust hit

The symbols array, drawn by line

A line axis from 1 to 20 with four declarations: struct SegmentReader at line 3, function open at line 8, function checkpoint_journal_offset at line 12, function merge_segments at line 17. merge_segments is the defs hit. 1 5 10 15 20 struct SegmentReader 3 function open 8 function checkpoint_journal_offset 12 function merge_segments 17
The 4 entries in the symbols array of src/lib.rs, each with a kind, a name and the line of its declaration. The axis is drawn to line 20. A rename tool starts from these lines.

On this 3-file tree both queries returned 2 hits, so the definition query was not narrower here. The difference is the field searched and the fields returned. Case matters too. src/query.go declares MergeSegments, and merge_segments did not match it.

Where XERJ stops

XERJ extracts definitions only. It builds no call graph, no import graph and no cross-file reference resolution, so it does not find the callers of a symbol.

Step in a refactorTool
Find every declaration of a name across 36 languagesXERJ, match on defs
Find every textual occurrence, call sites includedXERJ, match on body
A rename that must be soundA language server or a compiler plugin
A structural rewriteast-grep

What an agent reads, against the lines grep prints, is on the local code search over MCP page.

Caps and failure behavior

Limits of the code extractor

What tree-sitter extraction covers

languages36Pinned by a unit test.
file size2 MiBPer source file. A larger file is refused, and the refusal names the cap.
symbols5,000Per file. The array stops at 5,000.
name length200 bytesA longer symbol name is skipped.
Caps from extract/code.rs. A file that a grammar fails to parse is indexed as plain text, not dropped.

The capture ran on 1 single-node XERJ process over 3 source files in 3 languages: rust, python and go. No large repository was measured for this page.

FAQ

What makes the search syntax-aware?

XERJ parses each source file with tree-sitter and writes the declared symbols into a defs field and a symbols array with a kind, a name and a line.

How do I search declarations only?

Send a match query on the defs field. In our capture merge_segments returned 2 files that declare the name, each with its symbols array.

Does XERJ find every caller of a function?

No. Extraction is definitions only, with no call graph and no cross-file reference resolution. Use a language server when a rename must be sound.

Which languages does XERJ parse?

36 languages through tree-sitter, including Rust, Go, Python, TypeScript, Java and C++. Our capture exercised rust, python and go.

Does case matter in a symbol query?

Yes, in practice. src/query.go declares MergeSegments, and the query token merge_segments did not match it in our capture.

What happens if a file fails to parse?

Nothing is lost. If a grammar fails to parse a file, XERJ indexes the file as plain text, so the text query keeps working.

How large a source file can XERJ parse?

2 MiB per file. A larger file is refused, and the refusal names the cap. A file keeps at most 5,000 symbols, and a symbol name longer than 200 bytes is skipped.

Evidence

Related