1
0
mirror of https://codeberg.org/fediverse/fep.git synced 2026-08-05 19:55:46 +00:00

fep-f011: Full-Text Search Query Syntax for ActivityPub (#788)

Co-authored-by: Steve Bate <svc-github@stevebate.net>
Reviewed-on: https://codeberg.org/fediverse/fep/pulls/788
Co-authored-by: Steve Bate <steve-bate@noreply.codeberg.org>
Co-committed-by: Steve Bate <steve-bate@noreply.codeberg.org>
This commit is contained in:
Steve Bate
2026-03-17 18:46:27 +01:00
committed by silverpill
parent 13a1f68e13
commit 7b86e20b33
3 changed files with 350 additions and 0 deletions
+100
View File
@@ -0,0 +1,100 @@
// Tested with: http://lab.antlr.org/
grammar Query;
// ================== Parser rules ==================
// New explicit start rule
query
: expr EOF
;
expr
: or_expr
;
or_expr
: and_expr (OR and_expr)*
;
and_expr
: unary ((AND unary) | unary)*
;
unary
: NOT unary
| primary
;
primary
: term
| phrase
| regex
| range
| facet_expr
| LPAREN expr RPAREN
;
facet_expr
: WORD COLON (primary | LPAREN expr RPAREN)
;
range
: (LBRACK | LBRACE) bound TO bound (RBRACK | RBRACE)
;
bound
: WORD
| phrase
;
term
: WORD
;
// Parser-level wrappers for PHRASE and REGEX tokens
phrase
: PHRASE
;
regex
: REGEX
;
// ================== Lexer rules ==================
// Keywords
AND : 'AND';
OR : 'OR';
NOT : 'NOT';
TO : 'TO';
// Delimiters / punctuation
COLON : ':';
LPAREN : '(';
RPAREN : ')';
LBRACK : '[';
RBRACK : ']';
LBRACE : '{';
RBRACE : '}';
// WORD: run of non-space, non-delimiter, non-quote, non-slash characters.
WORD
: ~[ \t\r\n"/:(){}[\]]+
;
// phrase := '"' <any-char-except-quote>* '"'
PHRASE
: '"' (~["\r\n])+ '"'
;
// regex := '/' <regex-body> '/'
REGEX
: '/' (~[/\r\n])+ '/'
;
// All whitespace skipped
WS
: [ \t\r\n]+ -> skip
;
+17
View File
@@ -0,0 +1,17 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
{
"fepf011": "https://w3id.org/fep/f011#",
"endpoints": {
"@id": "https://www.w3.org/ns/activitypub#endpoints",
"@context": {
"search": {
"@id": "fepf011:search",
"@type": "@id"
}
}
}
}
]
}
+233
View File
@@ -0,0 +1,233 @@
---
slug: "f011"
authors: Steve Bate <svc-fep@stevebate.net>
status: DRAFT
dateReceived: 2026-03-17
discussionsTo: https://codeberg.org/steve-bate/fep/issues
---
# FEP-f011: Full-Text Search Query Syntax for ActivityPub
## Summary
This proposal defines a full-text search syntax for ActivityPub client support.
The syntax supports:
- Terms and quoted phrases
- Faceted search (`facet:value`)
- Boolean operators (`AND`, `OR`, `NOT`)
- Parenthesized groups
- Implicit `AND` between adjacent expressions
- Regex literals (`/.../`)
- Inclusive, exclusive, and mixed range bounds (`[a TO b]`, `{a TO b}`, `[a TO b}`)
## Terminology
The term *query string* means the value of the `q` query parameter in an HTTP request
against a search endpoint.
The term *faceted expression* means `<facet>:expr`, where `expr` may be a term,
phrase, regex, range, or parenthesized subexpression and the *facet* is typically a JSON property path.
The term *unfaceted expression* means a term, phrase, regex, or group that does not
have a facet prefix.
## Query Syntax
### Grammar
The (simplified) grammar proposed by this FEP is:
```ebnf
query := or_expr
or_expr := and_expr ("OR" and_expr)*
and_expr := unary (("AND" unary) | (implicit_and unary))*
unary := "NOT" unary | primary
primary := term | phrase | regex | range | field_expr | "(" query ")"
field_expr := WORD ":" (primary | "(" query ")")
range := ("[" | "{") bound TO bound ("]" | "}")
bound := WORD | phrase
phrase := '"' <any-char-except-quote>* '"'
regex := "/" <regex-body> "/"
term := WORD
```
An AND is implicit when two primaries are adjacent. The "regex-body" SHOULD conform to [RFC9485] (Interoperable Regular Expression Format).
A full ABNF grammar is [available](fep-f011.abnf). This can be used with parser generators like [ANTLR](https://www.antlr.org/).
### Token Rules
- `AND`, `OR`, `NOT`, and `TO` are recognized as operators only in uppercase.
- `WORD` excludes whitespace and these delimiters: `()[]{}":/`.
- Regex literals use slash delimiters and support escaped slash characters.
### Operator Precedence
The parser evaluates operators in this order:
1. Parenthesized expressions
2. Unary `NOT`
3. `AND` (explicit and implicit)
4. `OR`
`AND` and `OR` are left-associative.
### Faceted Expressions
A fielded expression has this form:
```text
<facet>:value
<facet>:"some phrase"
<facet>:/xy.*/
<facet>:[10 TO 20]
<facet>:(cats OR dogs)
```
`<facet>:(...)` applies the nested expression to the facet value.
### Range Expressions
Ranges support inclusive and exclusive delimiters independently:
- `[a TO b]` inclusive lower and upper bounds
- `{a TO b}` exclusive lower and upper bounds
- `[a TO b}` inclusive lower, exclusive upper
- `{a TO b]` exclusive lower, inclusive upper
Unbounded bounds may use `*` where supported by the evaluator implementation
(e.g., `rank:[4 TO *]`).
## Search Endpoint Discovery
The search endpoint is specified using the `search` property of the ActivityPub actor `endpoints` object. For example:
```json
{
"@context": [
"https://w3id.org/fep/f011"
"https://www.w3.org/ns/activitystreams"
],
...
"endpoints": {
"search": "https://server.example/search"
}
}
```
## HTTP Usage
This syntax is used as the `q` parameter on the search endpoint.
```text
GET https://server.example/search?q=<query>
```
### Responses
Responses MUST be an ActivityPub `Collection` or `OrderedCollection`. Paging is optional. If an `OrderedCollection` is returned, the results SHOULD be considered ranked although the ranking criteria will not be known by the client ( possibilities are relevance/[BM25](https://en.wikipedia.org/wiki/Okapi_BM25), recency, etc.).
### Example 1: Simple Primary Term
Request: `cats`
```http
GET /search?q=cats HTTP/1.1
Host: social.example
Accept: application/activity+json
```
### Example 2: Faceted + Unfaceted phrase
Request: `tag:fediverse language:en "ActivityPub client search"`
```http
GET /search?q=tag%3Afediverse%20language%3Aen%20%22ActivityPub%20client%20search%22 HTTP/1.1
Host: social.example
Accept: application/activity+json
```
Response:
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "OrderedCollection",
"id": "https://social.example/search?q=tag:fediverse language:en \"ActivityPub client search\"",
"totalItems": 1,
"orderedItems": [
{
"id": "https://social.example/notes/1",
"type": "Note"
}
]
}
```
### Example 3: Boolean + Grouping
Request:v`(cats OR dogs) AND NOT testing`
```http
GET /search?q=%28cats%20OR%20dogs%29%20AND%20NOT%20testing HTTP/1.1
Host: social.example
Accept: application/activity+json
```
### Example 4: Regex
Request: `type:Person preferredUsername:/alic.*/`
```http
GET /search?q=type%3APerson%20preferredUsername%3A%2Ffedi.%2A%2F
Host: social.example
Accept: application/activity+json
```
This could be useful for auto completion. Using a `type:HashTag` search term could similarly be used for hashtag autocompletion.
### Example 5: Range
Request: `rank:[2 TO 4]`
```http
GET /search?q=rank%3A%5B2%20TO%204%5D HTTP/1.1
Host: social.example
Accept: application/activity+json
```
### Error Handling
Invalid syntax MUST result in an HTTP client error status.
Examples include:
- Missing closing `)`
- Missing value after `<facet>:`
- Unterminated regex literal
- Missing range delimiter
Implementations MAY include details in a problem response body.
## Interoperability Notes
- Operators are case-sensitive (`AND` is an operator, `and` is a term).
- Unfaceted regex and primary term evaluation depends on pre-configured text facets in the evaluation layer.
- Field capabilities are evaluator-specific
- This proposal standardizes syntax and parsing behavior, not ranking/scoring.
## Implementations
- [FIRM](https://github.com/steve-bate/firm)
## Copyright
CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
To the extent possible under law, the authors of this Fediverse Enhancement
Proposal have waived all copyright and related or neighboring rights to this work.
[ActivityPub]: https://www.w3.org/TR/activitypub/ "ActivityPub is a decentralized social networking protocol based upon the ActivityStreams 2.0 data format."
[RFC9485]: https://www.rfc-editor.org/rfc/rfc9485.html#name-pcre-re2-and-ruby-regexps "This document specifies I-Regexp, a flavor of regular expression that is limited in scope with the goal of interoperation across many different regular expression libraries."