Moderne Trigrep
Moderne Trigrep provides fast, indexed search across your entire codebase, delivering sub-second search times even across thousands of repositories. It operates within the context of a Moderne organization, so you can scope searches to specific business units, teams, or application portfolios.
Moderne Trigrep supports Sourcegraph query syntax and Comby structural patterns, so AI models already trained on those tools can use Moderne Trigrep without learning a new query language. Beyond standard Sourcegraph filters, Moderne Trigrep adds semantic filters like visibility:, extends:, and implements: powered by Lossless Semantic Tree (LST) metadata.
Search results can feed directly into OpenRewrite recipe execution, allowing you to use cheap text searches as prefilters for expensive code transformations.
You can run Moderne Trigrep locally via the CLI's mod search command or through an AI coding agent running against the MCP server.
Why use Moderne Trigrep
Traditional text search tools struggle with enterprise codebases for several reasons:
- Linear file scanning scales poorly as repositories multiply.
- Regular expressions without indexing are even worse (they require full file reads to evaluate patterns).
- They lack an understanding of code structure. For example, they treat a method named
getUserByIdin the same way they treat a comment that happens to mention it.
Moderne Trigrep addresses these challenges by building trigram indexes during the repository build process. Once indexed, queries run against pre-computed data structures rather than raw files.
The search engine also understands code structure, so you can filter by:
- Symbol type (class, method, field)
- Visibility (public, private, protected)
- Inheritance relationships (extends, implements)
- Return types and thrown exceptions
This combination of speed and structural awareness makes exploratory queries practical (the kind of queries you wouldn't even bother attempting with grep).
For example, consider the situation where you want to search for all public methods that return a List across 500 repositories. A naive grep run would need to read every Java file and manually parse method signatures. Moderne Trigrep, on the other hand, answers the same question in under a second.
Getting started
To get started with Moderne Trigrep, you'll first need to sync an organization to your local machine. mod git sync moderne downloads the LST of each repository, which is all mod search needs:
# Sync the Spring organization to the working-set directory
mod git sync moderne working-set --organization Spring
# Search for the term "KafkaTemplate" across them all
mod search working-set "KafkaTemplate"
Moderne Trigrep needs an LST for every repository it searches, because the trigram index is derived from the LST rather than from the source text. The first mod search over a repository converts its LST to the V3 format in place, if it isn't already, and writes the index as part of the conversion. You should expect that first search to take a while on a large repository. Every search after that runs in under a second, because the converted LST and its index are reused.
If a repository has no LST in Moderne - or you want to search your own local changes - clone the sources and build them yourself instead:
# Sync the Spring organization, cloning source code as well
mod git sync moderne working-set --organization Spring --with-sources
# Build an LST for every repository in the working-set directory
mod build working-set
# Search for the term "KafkaTemplate" across them all
mod search working-set "KafkaTemplate"
mod search skips any repository that still has no LST and reminds you to build one.
Below, we'll dive into various ways you can refine your searches based on your needs.
Three ways to search
Moderne Trigrep supports three query paradigms, each suited to different tasks. You can combine them freely in a single search.
Literal and Sourcegraph-style queries
The simplest searches are literal text matches. For example, searching for System.out.println finds every occurrence of that exact string.
To refine your search, you can combine terms with boolean logic:
- AND (implicit): Multiple terms are combined with AND. For example,
@Service getUserByIdonly matches files containing both. - OR: Use
orfor either term to match. For example,@Service or @Controllermatches files with either annotation. - NOT: Prefix with
-to exclude. Because the CLI intercepts leading-as an option marker, insert--between the path and the query:mod search . -- @RestController -testfinds files containing@RestControllerbut nottest. - Grouping: Parentheses control precedence. For example,
(@Service or @Controller) getUserByIdfinds files withgetUserByIdthat also have either annotation.
You can also combine literal searches with filters to narrow results by file path, repository, language, or semantic properties like visibility and inheritance.
Common literal queries
| Category | Command | What it finds |
|---|---|---|
| Logging | mod search working-set "System.out.println" | Console output that should use a logger |
| Logging | mod search working-set "e.printStackTrace()" | Exception handling that swallows stack traces |
| Logging | mod search working-set "log.debug" | Debug logging (useful for auditing verbosity) |
| Spring | mod search working-set "@Autowired" | Field injection (often flagged for constructor injection) |
| Spring | mod search working-set "@RequestMapping" | Legacy request mapping annotations |
| Spring | mod search working-set "extends WebSecurityConfigurerAdapter" | Deprecated Spring Security configuration |
| Spring | mod search working-set "@EnableWebSecurity" | Security configuration entry points |
| Kafka | mod search working-set "@KafkaListener" | Kafka consumer endpoints |
| Kafka | mod search working-set "KafkaTemplate" | Kafka producer usage |
| Kafka | mod search working-set "ConsumerConfig." | Consumer configuration constants |
| Security | mod search working-set "@PreAuthorize" | Method-level security annotations |
| Security | mod search working-set "BCryptPasswordEncoder" | Password encoding implementations |
| Security | mod search working-set "csrf().disable()" | Disabled CSRF protection |
| Security | mod search working-set "permitAll()" | Open endpoints without authentication |
| Cryptography | mod search working-set 'getInstance("SHA-1")' | Weak hash algorithms |
| Cryptography | mod search working-set 'getInstance("MD5")' | Broken hash algorithms |
| Cryptography | mod search working-set 'getInstance("DES")' | Weak encryption algorithms |
| Cryptography | mod search working-set "SecureRandom" | Cryptographic random number generation |
Regular expressions
When literal matching isn't enough, you can use regular expressions to match patterns rather than exact text. To do so, wrap your pattern in forward slashes. For example, /log\.(info|debug|warn)\(/ finds logging calls at any of three levels.
Moderne Trigrep optimizes regex searches by extracting literal fragments from your pattern to narrow down candidate files first, so most regex searches are nearly as fast as literal ones.
Common regex queries
| Category | Command | What it finds |
|---|---|---|
| Logging | mod search working-set '/log\.(trace|debug|info|warn|error)\(/' | All SLF4J/Logback logging calls |
| Spring | mod search working-set '/Logger\.(getLogger|getAnonymousLogger)/' | Logger instantiation |
| Spring | mod search working-set '/@(Get|Post|Put|Delete|Patch)Mapping/' | REST endpoint annotations |
| Spring | mod search working-set '/@Value\("\$\{[^}]+\}"\)/' | Property injection with SpEL |
| Spring | mod search working-set '/@(Service|Repository|Component|Controller)/' | Spring stereotype annotations |
| Spring | mod search working-set '/spring\.(datasource|jpa|security)\./' | Spring configuration properties |
| Kafka | mod search working-set '/kafka\.(bootstrap|consumer|producer)\./' | Kafka configuration properties |
| Kafka | mod search working-set '/@KafkaListener\([^)]*topics/' | Kafka listeners with topic definitions |
| Kafka | mod search working-set '/ProducerRecord<[^>]+>/' | Typed Kafka producer records |
| Security | mod search working-set '/password\s*=\s*"[^"]+"/' | Hardcoded passwords |
| Security | mod search working-set '/(api[_-]?key|apikey)\s*[:=]/' | API key assignments |
| Cryptography | mod search working-set '/Cipher\.getInstance\("[^"]+"\)/' | Cipher algorithm selection |
| Cryptography | mod search working-set '/MessageDigest\.getInstance\("(MD5|SHA-1)"\)/' | Weak digest algorithms |
| PQC (Post-Quantum Cryptography) | mod search working-set '/ML-KEM-(512|768|1024)/' | NIST ML-KEM parameter sets |
| PQC | mod search working-set '/ML-DSA-(44|65|87)/' | NIST ML-DSA parameter sets |
| PQC | mod search working-set '/(SLH-DSA|SPHINCS\+)/' | Hash-based signature schemes |
Structural pattern matching
When you need to match code shapes that regular expressions struggle with (like balanced parentheses or nested generics), you can use structural pattern matching. Instead of working with raw text, structural matching understands code as code.
The key concept is the hole, written as :[name]. A hole matches content and optionally captures it for reference. For example, the pattern logger.:[level](:[message]) matches any logger call, capturing the log level and message separately. Unlike the regex equivalent, this correctly handles nested parentheses in the message argument.
To use structural matching, add the struct: prefix (or its structural: alias) before your pattern (e.g., mod search working-set struct:'logger.:[level](:[message])'). Without this prefix, hole syntax is treated as literal text. In Sourcegraph mode you can also enter structural mode with patternType:structural, which lets bare terms in the rest of the query be parsed as Comby patterns.
You can use typed holes to constrain what they match:
:[name:e]matches a syntactically complete expression, respecting balanced parentheses, brackets, and braces. For example,doSomething(:[args:e])correctly matchesdoSomething(foo(bar), baz)where a naive regex would stop at the first closing paren.:[name:id]matches a valid identifier.:[name:stmt]matches to the next semicolon or newline.:[name:block]matches balanced braces, useful for capturing method bodies or control flow blocks.:[name:g]extends expression matching to include angle brackets, handling Java generics likeList<Map<String, Integer>>.
You can also add constraints to holes:
:[method~get.*]requires the captured text to match a regex pattern.:[level:!debug,trace]excludes specific values.:[port:1..65535]constrains to a numeric range.:[name?]makes the hole optional....or:[_]matches without capturing.
Structural matching also respects string and comment boundaries, so a pattern won't match text inside a string literal or comment unless you explicitly want it to.
Common structural queries
| Category | Command | What it finds |
|---|---|---|
| Logging | mod search working-set struct:'logger.:[level](:[msg] + :[rest])' | String concatenation in log calls (performance issue) |
| Logging | mod search working-set struct:'log.:[level:!trace,debug](:[args:e])' | Non-debug logging only |
| Logging | mod search working-set struct:'catch (:[ex:e] :[var:id]) { :[_].printStackTrace()' | Catch blocks that only print stack traces |
| Logging | mod search working-set struct:'if (log.isDebugEnabled()) :[body:block]' | Guarded debug logging |
| Spring | mod search working-set struct:'@RequestMapping(:[attrs:e])' | Request mappings with any attributes |
| Spring | mod search working-set struct:'@Autowired :[type:id] :[field:id]' | Field injection candidates |
| Spring | mod search working-set struct:'@Value(":[expr]") :[type:id] :[field:id]' | Value-injected fields |
| Spring | mod search working-set struct:'ResponseEntity<:[type:g]>' | Typed REST responses |
| Spring | mod search working-set struct:'@Transactional(:[attrs:e])' | Transactional methods with attributes |
| Kafka | mod search working-set struct:'kafkaTemplate.send(:[topic:e], :[payload:e])' | Kafka message sends |
| Kafka | mod search working-set struct:'@KafkaListener(topics = :[topics:e])' | Kafka listener topic bindings |
| Kafka | mod search working-set struct:'new ProducerRecord<>(:[args:e])' | Producer record construction |
| Kafka | mod search working-set struct:'ConsumerRecords<:[key:g], :[val:g]>' | Typed consumer records |
| Security | mod search working-set struct:'http.:[method](:[args:e]).permitAll()' | Endpoints with permitAll |
| Security | mod search working-set struct:'.antMatchers(:[patterns:e]).:[auth]()' | Ant matcher security rules |
| Security | mod search working-set struct:'@PreAuthorize(":[expr]")' | Method security expressions |
| Security | mod search working-set struct:'new BCryptPasswordEncoder(:[strength:e])' | BCrypt with configurable strength |
| Cryptography | mod search working-set struct:'Cipher.getInstance(:[algo:e], :[provider:e])' | Cipher with explicit provider |
| Cryptography | mod search working-set struct:'KeyFactory.getInstance(:[algo:e])' | Key factory algorithm selection |
| Cryptography | mod search working-set struct:'SecretKeySpec(:[key:e], :[algo:e])' | Secret key construction |
| PQC | mod search working-set struct:'KeyPairGenerator.getInstance(:[algo~ML-.*])' | ML-KEM/ML-DSA key generation |
| PQC | mod search working-set struct:'Signature.getInstance(:[algo:e], :[pqc~.*PQC.*])' | PQC signature with provider |
Choosing a query syntax
Moderne Trigrep supports two query language dialects: Sourcegraph and Zoekt. You can switch between them with the --syntax flag:
# Sourcegraph syntax (default), path: is a Sourcegraph alias for file:
mod search working-set 'path:*.java' @Autowired
# Zoekt syntax uses file: directly
mod search working-set --syntax zoekt 'file:*.java' @Autowired
The default is sourcegraph, which is what most online documentation and AI training data use. If you're already familiar with the original Zoekt query language, you can switch to zoekt to use its native syntax directly.
Differences between the two syntaxes
Both dialects share the same core query features: literal search, regex, boolean operators, filters like file:, lang:, sym:, and all of the semantic filters (visibility:, extends:, implements:, etc.). The differences are mostly syntactic shortcuts that Sourcegraph adds on top of Zoekt:
| Feature | Sourcegraph | Zoekt |
|---|---|---|
| Explicit AND keyword | foo AND bar | foo bar (implicit AND) |
| Path filter alias | path:*.java | file:*.java |
| Language shorthand | l:java | lang:java |
| Content prefix | content:"text" | "text" (bare literal) |
| Pattern type switching | patternType:regexp foo.bar | /foo.bar/ (slash-delimited) |
Example queries in both syntaxes
Filter by file path:
# Sourcegraph, path: is an alias for file:
mod search working-set 'path:*.java' @Deprecated
# Zoekt uses file: directly
mod search working-set --syntax zoekt 'file:*.java' @Deprecated
Explicit AND vs implicit AND:
# Sourcegraph supports the explicit AND keyword
mod search working-set KafkaTemplate AND @Autowired
# Zoekt allows implicit AND only (terms separated by space)
mod search working-set --syntax zoekt KafkaTemplate @Autowired
Using search as a prefilter for recipes
Running an OpenRewrite recipe across thousands of repositories can be expensive since each one requires parsing its LST and running visitor patterns across the entire tree. If you know a recipe will only affect files containing a specific pattern, you can search first to skip repositories that don't match.
To do this, use the --last-search flag on the run command:
mod search working-set KafkaTemplate or KafkaProducer or @SendTo
mod run working-set --recipe=io.moderne.java.spring.kafka.KafkaMigration --last-search
The first command runs in under a second, identifying which repositories contain any Kafka producer patterns. The second command then processes only those repositories, skipping everything else. If your search found matches in 47 repositories out of 10,000 total, the recipe run processes 47 instead of 10,000.
This is particularly useful for migrations. Before converting all usages of a deprecated API, you can verify the scope with an instant search. The results also serve as a checklist for verifying that the recipe transformed every occurrence.
If you need to reference an older search rather than the most recent one, you can do so by taking advantage of the unique search identifier each search gets: --search=<id>.
Output modes
The mod search command offers two options for how results are displayed.
Rich mode (default)
Rich mode gives you colorized output with the file path, line number, and a code snippet with matches highlighted:
mod search working-set "@Deprecated"
By default, output stops after 100 matching files. If you need more results, use the --max flag or a count: filter in the query.
Plain mode
Plain mode produces uncolored output optimized for AI coding agents like Claude Code or Cursor. Each match appears with its line number, two lines of context, and the class and method declarations that enclose it. That way an agent can tell where a match is without opening the file:
mod search working-set "@Deprecated" --output plain
Use plain mode when integrating with automated workflows or when you don't need the visual formatting.
mod search needs at least three plain characters in a row to use the index. A query without them, such as ab or a wildcard-only regex like /.*/, has to read every file in the working set instead. That can take minutes on a large working set.
If you try and run a mod search that violates this, you will be asked to confirm that's what you really want by running the command with the -y flag. If a script or an agent tries to run a command like this, it will fail with a message instead.
Query reference
This section provides a quick reference for all query operators, filters, and structural hole types.
Boolean operators
Terms separated by space are implicitly ANDed. The or keyword creates disjunction. Parentheses group terms. A leading - negates a term (requires -- between the path and the query, as in the NOT example above).
Filters
| Filter | Description | Example |
|---|---|---|
file: | Match file paths. You can write the path as a glob (file:*.java, file:src/**/*.java) or as a regular expression (file:\.kt$, file:^src/(main|test)/). Aliases: f:, path: (Sourcegraph). | file:src/**/*.java |
lang: | Filter by language. Aliases: language:, l: (Sourcegraph). | lang:java |
case: | Control case sensitivity (yes/no). Searches are case-sensitive by default, which is the opposite of Sourcegraph's default. | case:yes findById |
type: | Choose what a term matches against: file content (file, the default), file paths only (path), or symbol declarations only (symbol).type:symbol is easy to confuse with sym:, which matches declarations by name rather than by text. | type:symbol Person |
sym: | Match symbol declarations by name. A bare name (sym:add) matches every symbol whose own name is exactly add, in any class. A fully qualified name (sym:java.util.List.add) matches that one symbol. A partly qualified name needs .. for the packages it leaves out (sym:..List.add), and /regex/ matches against the simple name, or against the whole FQN when the pattern contains a dot. Declarations only: use call: or ref: to find usages. Alias: symbol:. | sym:UserRepository |
select: | Filter symbol matches by kind: symbol.class, symbol.interface, symbol.enum, symbol.method, symbol.constructor, symbol.field, symbol.record, symbol.javadoc-ref, symbol.comment, symbol.string-literal, plus language-specific kinds such as symbol.program and symbol.paragraph (COBOL) or symbol.job and symbol.step (JCL). | select:symbol.method |
scope: | Search only comments (comments), only string literals (strings), or everything except comments (code). Today only COBOL records where its comments and strings are. In other languages, scope:comments finds nothing and scope:code finds everything. | scope:comments TODO |
count: | Cap the total results returned by the query, or count:all to return every match. | count:500 |
content: | Match the given text in file content (Sourcegraph). Use it when a search term would otherwise be read as a filter name, such as content:"type:". | content:"text" |
patternType: | Sourcegraph-only. Switch how unquoted terms are read: keyword (default), literal, regexp, or structural. | patternType:regexp foo.bar |
visibility: | Filter by access modifier: public, private, protected, package (or package-private), or any (Sourcegraph; matches everything). | visibility:public |
static: | Filter for static modifier (yes/no). | static:yes |
final: | Filter for final modifier (yes/no). | final:yes |
abstract: | Filter for abstract modifier (yes/no). | abstract:yes |
extends: | Match classes that extend the given class, directly or through any intermediate superclass. | extends:BaseService |
implements: | Match classes that implement the given interface, directly or through a superclass or superinterface. | implements:Repository |
annotated: | Match declarations (class, method, or field) that carry the given annotation directly. Meta-annotations don't count, so a Spring @Service class does not match annotated:Component. | annotated:AutoConfiguration |
returns: | Match methods by return type. | returns:List |
throws: | Match methods by declared exceptions. | throws:IOException |
ref: | Match the places a type is named: variable and field declarations, generics, casts, new, and Javadoc links (like OpenRewrite's FindTypes). Narrow to one kind of use with ref.call:, ref.field:, and so on. See the ref: and call: filters. Alias: references:. | ref:java.util.List |
call: | Match method and constructor call sites by the called method's declaring type and name, subclass-aware. See the ref: and call: filters. Alias: calls:. | call:java.util.List.add |
dependency: | Match the calls and references whose target comes from a dependency, by name and optional version. See the dependency: filter. Alias: dep:. | dependency:guava:29.x |
The symbol-aware filters read from LST symbol metadata rather than raw text, which trigrep extracts from source code. They come in two groups:
- Declaration filters (
sym:,select:,type:symbol,visibility:,static:,final:,abstract:) read a symbol's name, kind, and modifiers off the parse tree, so they work across all supported source languages: Java, Kotlin, Groovy, Scala, Python, JavaScript, TypeScript, Go, C#, COBOL, JCL, and BMS. The mainframe languages declare their own symbol kinds (programs, paragraphs, jobs, steps, DD statements, and maps) and reference kinds such asref.pgm:,ref.perform:, andref.copy:. - Type-reference filters (
extends:,implements:,annotated:,returns:,throws:,ref:,call:,dependency:) resolve types through the LST, so how much they find depends on the language's type attribution, which is most complete for Java.
Configuration and data files (XML, JSON, YAML, and the like) are indexed for text only. Literal search, regex, and the text-oriented filters (file:, lang:, case:, content:, count:, patternType:) work everywhere, but the symbol-aware filters above do not apply to text-only files.
The filters repo:, branch:, rev:, context:, fork:, archived:, and timeout: are parsed but have no effect: they emit a warning and are dropped from the query. An index covers one repository's working tree, so there is no other repository or branch to select. mod search scopes a search by the directory you give it instead.
The CLI treats a single-arg query as a literal phrase and re-emits it quoted in the Searching for: line. As a result, multi-token queries wrapped in a single pair of shell quotes lose their Sourcegraph semantics and almost always return 0 matches. Examples:
'@Service or @Controller'is searched as the literal phrase@Service or @Controller(which appears nowhere), so 0 matches.'visibility:public returns:List'is parsed as a single filter with valuepublic returns:List, so it fails withUnknown visibility: public returns:List.'KafkaTemplate -test'is searched as the literal phraseKafkaTemplate -test, so 0 matches. To use-negation, pass terms unquoted with--:mod search . -- KafkaTemplate -test.
Pass each token as its own argument, or quote each individually:
# Works
mod search . @Service or @Controller
mod search . visibility:public returns:List
mod search . "@Service" or "@Controller"
# Fails (literal phrase)
mod search . "@Service or @Controller"
mod search . 'visibility:public returns:List'
Quoting a single token to protect it from the shell (e.g. "file:**/*Test.java" to prevent glob expansion) is fine.
Specifying a type or symbol in a filter
Several filters take the name of a type or symbol: sym:, extends:, implements:, annotated:, returns:, throws:, ref:, and call:. The name can be fully qualified, a simple name, or a glob:
| You write | It matches |
|---|---|
implements:java.io.Serializable | exactly that type |
implements:Serializable | a type named Serializable in any package |
extends:java.lang.* | any class in the java.lang package (* is one name) |
call:*..ImmutableList.of | ImmutableList.of in any package (.. spans packages) |
type:symbol versus sym:
type:symbol and sym: both look at symbols, but they are not synonyms.
type:symbol Person matches the text Person at declaration sites only: class headers, method and constructor signatures, and field declarations. That includes class PersonRepository, Person currentPerson;, and void savePerson(Person p). Uses inside method bodies, imports, and comments do not match.
sym:Person matches every symbol whose own name is exactly Person, such as the class com.example.Person. It doesn't match PersonRepository, and it doesn't match members of Person such as Person.getFirstName, because a bare name has to equal the symbol's final name segment. To reach a member, qualify the name (sym:..Person.getFirstName). The match is limited to declarations, so it won't surface usages, string literals, or comments.
A bare Person is broader than either. It matches every textual occurrence, including imports, comments, and string literals.
The type:symbol restriction only applies to literal terms. A regex such as type:symbol /Person/ currently bypasses it and searches the full file.
The ref: and call: filters
ref: and call: resolve each occurrence through the LST type tables rather than matching text, so they're subclass-aware.
ref:T, where a type is used. Every occurrence the index records carries a kind. Bare ref:T matches where T appears as a type (a written type-name position, a new T(), or a Javadoc link), the same surface as OpenRewrite's FindTypes. It deliberately leaves out references that are merely an expression of type T (a method call or field access on a T value), which would otherwise flood a search for a common type like List. Narrow or widen to specific kinds with a dotted qualifier:
| Query | Matches |
|---|---|
ref:T | T as a type: type-name positions, new T(), or Javadoc links (the default; ref.usage: is an explicit alias) |
ref.type:T | a written type-name position only |
ref.new:T | a new T() constructor call |
ref.doc:T | a Javadoc {@link} or @see |
ref.call:T | a method call on a T value (by receiver / declaring type) |
ref.field:T | a field access whose owner is T |
ref.fieldtype:T | a field-access expression whose result type is T (this.list where list is a List) |
ref.owner:T | members declared in T (its surface), not uses of it |
ref.[type,call]:T | a union of the listed kinds |
ref.any:T | every reference kind, no filtering |
call:Type.method, where a method is called. call: finds the places that call a method, named by the type that declares it: call:java.util.List.add finds every call to List.add. Constructors use <init>, so call:java.util.ArrayList.<init> finds every new ArrayList<>(). Subtypes count too. call:java.util.Collection.add also matches calls made through a List or an ArrayList, because each call is indexed against every supertype that declares the same method, including types from the JDK and your dependencies. Values take the same forms as the type filters: a fully qualified name, an AspectJ glob, or /regex/. One thing call: can't do is tell overloads apart, because the index stores a method's name but not its parameters. For that, use a recipe (see Limitations and when to use recipes).
The dependency: filter
dependency: finds every call and reference to a given library. You don't need to know which packages or classes the library contains. Write its name the way you would in a build file, with an optional version at the end. Any part of the name can be a glob.
| Query | Matches |
|---|---|
dependency:guava | every use of an artifact, group, or package named guava |
dependency:com.fasterxml.* | every use of a group matching the glob |
dependency:com.google.guava:guava | a Maven group:artifact |
dependency:guava:29.x | Guava 29, any patch level (29.0-jre included) |
dependency:com.fasterxml.*:jackson*:2.15.x | Jackson 2.15 artifacts in any com.fasterxml group |
dependency:lodash@4.x | an npm package by name@version, including @types/* scopes |
dependency:"log4j-core:[2.0,2.17.1)" | a Maven version range (quotes are needed due to the use of [ and )) |
The version doesn't have to be exact. 2.15 and 2.15.x both mean any 2.15 release. Ranges like ^2.15 and ~2.15.1 work too. A glob such as *-jre matches against the version text as written. Go modules (github.com/gin-gonic/*@1.9.x), pip packages, and NuGet packages follow the same pattern.
On its own, dependency: matches every way code can use a library, including method calls on values whose type was inferred rather than written out. This makes it broader than ref:, which only matches where a type is written by name. To narrow it to one kind of use, add the same qualifiers ref: takes, such as dependency.call: or dependency.[type,new]:.
Transitive dependencies match under their own name. A JsonProcessingException is found by dependency:jackson-core even if your build only declares jackson-databind. Types from the JDK and from the repository itself never match.
The filter combines with the others. For example, this query finds every call to ImmutableList.of that resolves to Guava 29:
mod search working-set dependency:guava:29.x call:*..ImmutableList.of
Older CLIs did not record which dependency a reference came from. If an LST was built or converted before 4.9.1, dependency: finds nothing in it. Rebuild it with mod build, or sync it again from Moderne.
Structural holes
These holes require the struct: prefix (see structural pattern matching).
| Syntax | Description |
|---|---|
:[name] | Matches anything non-greedily |
:[name:e] | Matches balanced expressions |
:[name:id] | Matches identifiers |
:[name:stmt] | Matches statements |
:[name:block] | Matches balanced braces |
:[name:g] | Matches generics (includes angle brackets) |
:[name:ws] | Matches whitespace |
:[name~pattern] | Constrains by regex |
:[name:!a,b] | Excludes specific values |
:[name?] | Makes the hole optional |
:[name:1..100] | Constrains to numeric range |
... or :[_] | Matches without capturing |
Limitations and when to use recipes
While Moderne Trigrep offers a lot of flexibility in how to search, there are some questions that don't fit cleanly in a search query (e.g., whether user input can reach a SQL query, or which calls to List.add use the two-argument version). Below, we'll go into more detail about when you should reach for a recipe.
| If you want to... | What search will return | What recipe to use instead |
|---|---|---|
Find calls to one version of an overloaded method, such as the two-argument List.add | Every call to List.add. The index keeps a method's name but not its parameters. | FindMethods with a method pattern such as java.util.List add(int, ..) |
| Find every use of a type or any of its subtypes | Only the type you named. returns:List skips a method that returns ArrayList, and ref:java.util.Collection skips a List. | FindTypes with checkAssignability set to true |
Find classes picked up through a meta-annotation, such as everything behind Spring's @Configuration | Only annotations written directly on the declaration, so annotated:Configuration misses an @AutoConfiguration class. | FindAnnotations with matchMetaAnnotations set to true |
| Find calls to deprecated library methods | Only the places in your own code that write @Deprecated. | FindDeprecatedUses |
| Learn where a value came from, whether user input reaches a SQL query, or which methods transitively call another | Each match on its own. A query can't relate one line to another. | A recipe with control flow or data flow analysis, or a scanning recipe for whole-repository questions |
| Get a data table you can query or aggregate | Files, lines, and snippets. | A search recipe such as FindMethods, whose data table you can query with mod study |
| Fix the matches, not just list them | Nothing. Search is read-only. | A transformation recipe, with search as the prefilter |
When to start with search
Despite these limitations, Moderne Trigrep is still valuable as a first step in many workflows. You can use it to gauge the scope of a migration before writing or running a recipe. For example, a quick search for extends:AbstractController tells you how many classes inherit from a base class you plan to refactor.
Moderne Trigrep also helps during recipe development. When you're building a recipe to find a specific pattern, you can use Moderne Trigrep to locate examples in real code first. These examples become test cases and help you understand the variations you need to handle.
How trigram indexing works
Trigram indexing has been referenced in research papers since the 1940s. In 2012, Russ Cox described the approach he used to build Google Code Search. Google later open-sourced Zoekt, a trigram-based search engine, which Sourcegraph subsequently forked. Moderne Trigrep is a from-scratch Java implementation that is compatible with Sourcegraph's query syntax but does not share any code with the Google or Sourcegraph implementations.
A trigram is a sequence of three consecutive characters. A sliding window moves across the text, extracting each overlapping triple:
g e t M e s s a g e
└─┬─┘ → "get"
└─┬─┘ → "etM"
└─┬─┘ → "tMe"
└─┬─┘ → "Mes"
└─┬─┘ → "ess"
└─┬─┘ → "ssa"
└─┬─┘ → "sag"
└─┬─┘ → "age"
During indexing, every position where each trigram appears is recorded. When you search for getMessage, the engine looks up the posting lists for each trigram and intersects them. Only files containing all required trigrams at consecutive positions are candidates for a full match. Most files are rejected by the index lookup alone, which is why trigram search is so fast.
Even in a codebase with millions of files, the intersection of posting lists typically narrows the search to a handful of files that need actual verification. A typical trigram index runs about 10-20% of the original source code size.
Moderne Trigrep generates its indexes from LSTs rather than raw source code, which gives the index access to symbol information, type data, and other semantic details that wouldn't be available from text alone. This is what powers semantic filters like sym:, extends:, and visibility:. Each symbol's related types are stored as compact V3 type-table slots (resolved at search time through the source set's type tables) rather than duplicated inline strings, so a query like sym:, extends:, or implements: narrows the candidate file set before any byte reads.
The index is written at the same time as the V3 LST and lives next to it. Look under .moderne/build/{buildId}/ for the .zoekt files: there's one shard per source set plus a merged index for the whole repository. A search never sees a half-written index, because the CLI marks it as complete only after the last file is written on disk. Each shard also keeps a copy of the file's content, which is why you can search repositories you have only synced LSTs for, without cloning their source. To refresh the index after significant code changes, re-run mod build.
Further reading
Since Moderne Trigrep supports both Sourcegraph and Zoekt query syntax as well as Comby-style structural patterns, documentation for those tools applies directly here:
- Sourcegraph query syntax covers the full Sourcegraph query language including boolean operators, filters, and regex patterns. This is the default syntax for
mod search. - Zoekt query syntax documents the original Zoekt query language. Use
--syntax zoektto switch to this dialect. - Comby documentation explains hole types and matching semantics in depth. The Comby playground lets you experiment with patterns interactively.
- How Trigram Indexing Works by Russ Cox explains the theory behind trigram indexes.
Next steps
- Moderne CLI reference for the full
mod searchcommand documentation. - Type-aware code search for the web-based search experience on the Moderne Platform.
- Recipe authoring fundamentals to learn how to build recipes for the searches Trigrep can't express, such as matching one overload or relating results across files.