Repository: grofers/osquery
Pull request #9: feat: add support for function_score clause in osquery package
Author: login: harshit98
Created: 2025-05-10T15:29:18Z | Merged: 2025-05-14T06:35:25Z

## Key Changes

### Function Score Support

- Added a new file `function_score.go` to implement function score queries.
- Introduced the `FunctionScoreQuery` struct and relevant methods:
    - FunctionScore(): Initializes a function score query.
    - AddFunction(Function): Adds scoring functions like RandomScore.
    - BoostMode(mode): Configures boosting mode.
    - Map(): Maps the function score query to a structured format.
- Added support for scoring functions:
    - RandomScoreFunction: Supports random scoring with optional `seed` and `field` parameters.
    - Additional scoring functions like `FieldValueFactorFunction` outlined for future use.

### Unit Tests for Function Score

- Added the `function_score_test.go` file.
- Added test cases for different `function_score` scenarios:

### Basic Function Score Queries
- Queries with boost modes, multiple functions, and seeds.
- Queries with `match_all` and `term` queries.

### Updated Documentation:

- Updated comments in `query_nested.go` to reflect proper struct and constant descriptions for ScoreModeType.
Sorting Parameters Support:
- Updated `sort_test.go` to support advanced sorting parameters like `mode`, `nested_path`, and `nested_filter`.

### References
For more details on the `function_score` query, see: [OpenSearch Documentation: Function Score Query](https://docs.opensearch.org/docs/latest/query-dsl/compound/function-score/)

### Testing
Unit tests have been added.


Diff:
diff --git a/function_score.go b/function_score.go
new file mode 100644
index 0000000..1ab6e61
--- /dev/null
+++ b/function_score.go
@@ -0,0 +1,155 @@
+// Package osquery Modified by harshit98 on 2025-05-10
+// Changes: Added function score support
+package osquery
+
+type FunctionScoreQuery struct {
+	query     Mappable
+	functions []Function
+	boostMode string
+	scoreMode string
+	maxBoost  *float32
+	minScore  *float32
+	boost     *float32
+}
+
+type Function interface {
+	Map() map[string]interface{}
+}
+
+type RandomScoreFunction struct {
+	seed  *int64
+	field string
+}
+
+type ScriptScoreFunction struct {
+	script *ScriptField
+}
+
+// Additional functions can be added as per usecase in future like:
+// - ScriptScoreFunction
+// - DecayFunction (with variants for geo, date, numeric)
+// - WeightFunction
+//
+// For ref: https://docs.opensearch.org/docs/latest/query-dsl/compound/function-score/
+
+func FunctionScore(query Mappable) *FunctionScoreQuery {
+	return &FunctionScoreQuery{query: query}
+}
+
+func (q *FunctionScoreQuery) Function(f Function) *FunctionScoreQuery {
+	q.functions = append(q.functions, f)
+	return q
+}
+
+func (q *FunctionScoreQuery) BoostMode(boostMode string) *FunctionScoreQuery {
+	q.boostMode = boostMode
+	return q
+}
+
+func (q *FunctionScoreQuery) ScoreMode(scoreMode string) *FunctionScoreQuery {
+	q.scoreMode = scoreMode
+	return q
+}
+
+func (q *FunctionScoreQuery) MaxBoost(maxBoost float32) *FunctionScoreQuery {
+	q.maxBoost = &maxBoost
+	return q
+}
+
+func (q *FunctionScoreQuery) MinScore(minScore float32) *FunctionScoreQuery {
+	q.minScore = &minScore
+	return q
+}
+
+func (q *FunctionScoreQuery) Boost(boost float32) *FunctionScoreQuery {
+	q.boost = &boost
+	return q
+}
+
+func (q *FunctionScoreQuery) Map() map[string]interface{} {
+	m := make(map[string]interface{})
+
+	if q.query != nil {
+		m["query"] = q.query.Map()
+	}
+
+	if len(q.functions) > 0 {
+		funcs := make([]map[string]interface{}, len(q.functions))
+		for i, f := range q.functions {
+			funcs[i] = f.Map()
+		}
+		m["functions"] = funcs
+	}
+
+	if q.boostMode != "" {
+		m["boost_mode"] = q.boostMode
+	}
+
+	if q.maxBoost != nil {
+		m["max_boost"] = *q.maxBoost
+	}
+
+	if q.scoreMode != "" {
+		m["score_mode"] = q.scoreMode
+	}
+
+	if q.minScore != nil {
+		m["min_score"] = *q.minScore
+	}
+
+	if q.boost != nil {
+		m["boost"] = *q.boost
+	}
+
+	return map[string]interface{}{
+		"function_score": m,
+	}
+}
+
+func RandomScore() *RandomScoreFunction {
+	return &RandomScoreFunction{}
+}
+
+func (f *RandomScoreFunction) Seed(seed int64) *RandomScoreFunction {
+	f.seed = &seed
+	return f
+}
+
+func (f *RandomScoreFunction) Field(field string) *RandomScoreFunction {
+	f.field = field
+	return f
+}
+
+func (f *RandomScoreFunction) Map() map[string]interface{} {
+	m := make(map[string]interface{})
+
+	if f.seed != nil {
+		m["seed"] = *f.seed
+	}
+
+	if f.field != "" {
+		m["field"] = f.field
+	}
+
+	return map[string]interface{}{
+		"random_score": m,
+	}
+}
+
+func FunctionScriptScore(script *ScriptField) *ScriptScoreFunction {
+	return &ScriptScoreFunction{script: script}
+}
+
+func (f *ScriptScoreFunction) Map() map[string]interface{} {
+	if f.script == nil {
+		return map[string]interface{}{
+			"script_score": map[string]interface{}{},
+		}
+	}
+	scriptMap := f.script.Map()["script"].(map[string]interface{})
+	return map[string]interface{}{
+		"script_score": map[string]interface{}{
+			"script": scriptMap,
+		},
+	}
+}
diff --git a/function_score_test.go b/function_score_test.go
new file mode 100644
index 0000000..ce68ad9
--- /dev/null
+++ b/function_score_test.go
@@ -0,0 +1,233 @@
+package osquery
+
+import (
+	"testing"
+)
+
+func TestFunctionScore(t *testing.T) {
+	runMapTests(t, []mapTest{
+		{
+			"function_score query with random_score function",
+			FunctionScore(Term("user", "kimchy")).
+				Function(RandomScore()),
+			map[string]interface{}{
+				"function_score": map[string]interface{}{
+					"query": map[string]interface{}{
+						"term": map[string]interface{}{
+							"user": map[string]interface{}{
+								"value": "kimchy",
+							},
+						},
+					},
+					"functions": []map[string]interface{}{
+						{
+							"random_score": map[string]interface{}{},
+						},
+					},
+				},
+			},
+		},
+		{
+			"function_score query with random_score function and boost_mode",
+			FunctionScore(Term("user", "kimchy")).
+				Function(RandomScore()).
+				BoostMode("sum"),
+			map[string]interface{}{
+				"function_score": map[string]interface{}{
+					"query": map[string]interface{}{
+						"term": map[string]interface{}{
+							"user": map[string]interface{}{
+								"value": "kimchy",
+							},
+						},
+					},
+					"functions": []map[string]interface{}{
+						{
+							"random_score": map[string]interface{}{},
+						},
+					},
+					"boost_mode": "sum",
+				},
+			},
+		},
+		{
+			"function_score query with random_score function with seed",
+			FunctionScore(Term("user", "kimchy")).
+				Function(RandomScore().Seed(42)),
+			map[string]interface{}{
+				"function_score": map[string]interface{}{
+					"query": map[string]interface{}{
+						"term": map[string]interface{}{
+							"user": map[string]interface{}{
+								"value": "kimchy",
+							},
+						},
+					},
+					"functions": []map[string]interface{}{
+						{
+							"random_score": map[string]interface{}{
+								"seed": int64(42),
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			"function_score query with random_score function with field",
+			FunctionScore(Term("user", "kimchy")).
+				Function(RandomScore().Field("_seq_no")),
+			map[string]interface{}{
+				"function_score": map[string]interface{}{
+					"query": map[string]interface{}{
+						"term": map[string]interface{}{
+							"user": map[string]interface{}{
+								"value": "kimchy",
+							},
+						},
+					},
+					"functions": []map[string]interface{}{
+						{
+							"random_score": map[string]interface{}{
+								"field": "_seq_no",
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			"function_score query with multiple functions",
+			FunctionScore(Term("user", "kimchy")).
+				Function(RandomScore()).
+				Function(RandomScore().Seed(123)),
+			map[string]interface{}{
+				"function_score": map[string]interface{}{
+					"query": map[string]interface{}{
+						"term": map[string]interface{}{
+							"user": map[string]interface{}{
+								"value": "kimchy",
+							},
+						},
+					},
+					"functions": []map[string]interface{}{
+						{
+							"random_score": map[string]interface{}{},
+						},
+						{
+							"random_score": map[string]interface{}{
+								"seed": int64(123),
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			"function_score query with match_all query",
+			FunctionScore(MatchAll()).
+				Function(RandomScore()),
+			map[string]interface{}{
+				"function_score": map[string]interface{}{
+					"query": map[string]interface{}{
+						"match_all": map[string]interface{}{},
+					},
+					"functions": []map[string]interface{}{
+						{
+							"random_score": map[string]interface{}{},
+						},
+					},
+				},
+			},
+		},
+		{
+			"function_score query with script_score function",
+			FunctionScore(Term("user", "kimchy")).
+				Function(FunctionScriptScore(Script("my_script").Source("doc['my_field'].value * 2.0"))),
+			map[string]interface{}{
+				"function_score": map[string]interface{}{
+					"query": map[string]interface{}{
+						"term": map[string]interface{}{
+							"user": map[string]interface{}{
+								"value": "kimchy",
+							},
+						},
+					},
+					"functions": []map[string]interface{}{
+						{
+							"script_score": map[string]interface{}{
+								"script": map[string]interface{}{
+									"source": "doc['my_field'].value * 2.0",
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			"function_score query with script_score function with params",
+			FunctionScore(Term("user", "kimchy")).
+				Function(FunctionScriptScore(Script("my_script").
+					Source("doc['my_field'].value * params.factor").
+					Params(ScriptParams{"factor": 2.0}).
+					Lang("painless"))),
+			map[string]interface{}{
+				"function_score": map[string]interface{}{
+					"query": map[string]interface{}{
+						"term": map[string]interface{}{
+							"user": map[string]interface{}{
+								"value": "kimchy",
+							},
+						},
+					},
+					"functions": []map[string]interface{}{
+						{
+							"script_score": map[string]interface{}{
+								"script": map[string]interface{}{
+									"source": "doc['my_field'].value * params.factor",
+									"params": map[string]interface{}{
+										"factor": 2.0,
+									},
+									"lang": "painless",
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	})
+}
+
+func TestFunctionScoreWithQuery(t *testing.T) {
+	runMapTests(t, []mapTest{
+		{
+			"query with function_score",
+			Query(
+				FunctionScore(Term("user", "kimchy")).
+					Function(RandomScore()).
+					BoostMode("sum"),
+			),
+			map[string]interface{}{
+				"query": map[string]interface{}{
+					"function_score": map[string]interface{}{
+						"query": map[string]interface{}{
+							"term": map[string]interface{}{
+								"user": map[string]interface{}{
+									"value": "kimchy",
+								},
+							},
+						},
+						"functions": []map[string]interface{}{
+							{
+								"random_score": map[string]interface{}{},
+							},
+						},
+						"boost_mode": "sum",
+					},
+				},
+			},
+		},
+	})
+}
diff --git a/query_nested.go b/query_nested.go
index 732168e..2aabca4 100644
--- a/query_nested.go
+++ b/query_nested.go
@@ -1,4 +1,4 @@
-// Modified by bforbhartiii on 2025-02-24
+// Package osquery Modified by bforbhartiii on 2025-02-24
 // Changes: Added nested query support
 package osquery
 
@@ -7,19 +7,19 @@ import "github.com/fatih/structs"
 type ScoreModeType string
 
 const (
-	// Uses the average relevance score of all matching inner documents
+	// ScoreModeAvg Uses the average relevance score of all matching inner documents
 	ScoreModeAvg ScoreModeType = "avg"
 
-	// Assigns the highest relevance score from the matching inner documents to the parent.
+	// ScoreModeMax Assigns the highest relevance score from the matching inner documents to the parent.
 	ScoreModeMax ScoreModeType = "max"
 
-	// Assigns the lowest relevance score from the matching inner documents to the parent.
+	// ScoreModeMin Assigns the lowest relevance score from the matching inner documents to the parent.
 	ScoreModeMin ScoreModeType = "min"
 
-	// Sums the relevance scores of all matching inner documents.
+	// ScoreModeSum Sums the relevance scores of all matching inner documents.
 	ScoreModeSum ScoreModeType = "sum"
 
-	// Ignores the relevance scores of inner documents and assigns a score of 0 to the parent document.
+	// ScoreModeNone Ignores the relevance scores of inner documents and assigns a score of 0 to the parent document.
 	ScoreModeNone ScoreModeType = "none"
 )
 
diff --git a/sort_test.go b/sort_test.go
index 663e1a0..d68a6d4 100644
--- a/sort_test.go
+++ b/sort_test.go
@@ -1,4 +1,5 @@
-// Package osquery provides a query builder for OpenSearch.
+// Package osquery Modified by harshit98 on 2025-05-07
+// Changes: Added sort params support like mode, nested_path, nested_filter
 package osquery
 
 import (


Reviews:
user: login: 17sushmita
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T07:57:36Z
user: login: bforbhartiii
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-13T07:37:48Z
user: login: harshit98
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T12:14:55Z

Review comments:
diff_hunk: @@ -0,0 +1,104 @@
+// Package osquery Modified by harshit98 on 2025-05-10
+// Changes: Added function score support
+package osquery
+
+type FunctionScoreQuery struct {
+	query     Mappable
+	functions []Function
+	boostMode string
+	scoreMode string
+	maxBoost  float32
+	minScore  float32
+	boost     float32
+}
+
+type Function interface {
+	Map() map[string]interface{}
+}
+
+type RandomScoreFunction struct {
+	seed  *int64
+	field string
+}
+
+type FieldValueFactorFunction struct {
+	field    string
+	factor   float32
+	modifier string
+	missing  float32
+}
+
+// Additional functions can be added as per usecase in future like:
+// - ScriptScoreFunction
+// - DecayFunction (with variants for geo, date, numeric)
+// - WeightFunction
+//
+// For ref: https://docs.opensearch.org/docs/latest/query-dsl/compound/function-score/
+
+func FunctionScore(query Mappable) *FunctionScoreQuery {
+	return &FunctionScoreQuery{query: query}
+}
+
+func (q *FunctionScoreQuery) AddFunction(f Function) *FunctionScoreQuery {
path: function_score.go
user: login: 17sushmita
body: ```suggestion
func (q *FunctionScoreQuery) Function(f Function) *FunctionScoreQuery {
```
created_at: 2025-05-12T07:57:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,104 @@
+// Package osquery Modified by harshit98 on 2025-05-10
+// Changes: Added function score support
+package osquery
+
+type FunctionScoreQuery struct {
+	query     Mappable
+	functions []Function
+	boostMode string
+	scoreMode string
+	maxBoost  float32
+	minScore  float32
+	boost     float32
+}
+
+type Function interface {
+	Map() map[string]interface{}
+}
+
+type RandomScoreFunction struct {
+	seed  *int64
+	field string
+}
+
+type FieldValueFactorFunction struct {
+	field    string
+	factor   float32
+	modifier string
+	missing  float32
+}
+
+// Additional functions can be added as per usecase in future like:
+// - ScriptScoreFunction
+// - DecayFunction (with variants for geo, date, numeric)
+// - WeightFunction
+//
+// For ref: https://docs.opensearch.org/docs/latest/query-dsl/compound/function-score/
+
+func FunctionScore(query Mappable) *FunctionScoreQuery {
+	return &FunctionScoreQuery{query: query}
+}
+
+func (q *FunctionScoreQuery) Function(f Function) *FunctionScoreQuery {
+	q.functions = append(q.functions, f)
+	return q
+}
+
+func (q *FunctionScoreQuery) BoostMode(mode string) *FunctionScoreQuery {
+	q.boostMode = mode
+	return q
+}
path: function_score.go
user: login: bforbhartiii
body: functions missing to add scoreMode, maxBoost, minScore and score
created_at: 2025-05-13T07:21:43Z
author_association: NONE
diff_hunk: @@ -0,0 +1,104 @@
+// Package osquery Modified by harshit98 on 2025-05-10
+// Changes: Added function score support
+package osquery
+
+type FunctionScoreQuery struct {
+	query     Mappable
+	functions []Function
+	boostMode string
+	scoreMode string
+	maxBoost  float32
+	minScore  float32
+	boost     float32
+}
+
+type Function interface {
+	Map() map[string]interface{}
+}
+
+type RandomScoreFunction struct {
+	seed  *int64
+	field string
+}
+
+type FieldValueFactorFunction struct {
+	field    string
+	factor   float32
+	modifier string
+	missing  float32
+}
+
+// Additional functions can be added as per usecase in future like:
+// - ScriptScoreFunction
+// - DecayFunction (with variants for geo, date, numeric)
+// - WeightFunction
+//
+// For ref: https://docs.opensearch.org/docs/latest/query-dsl/compound/function-score/
+
+func FunctionScore(query Mappable) *FunctionScoreQuery {
+	return &FunctionScoreQuery{query: query}
+}
+
+func (q *FunctionScoreQuery) Function(f Function) *FunctionScoreQuery {
+	q.functions = append(q.functions, f)
+	return q
+}
+
+func (q *FunctionScoreQuery) BoostMode(mode string) *FunctionScoreQuery {
+	q.boostMode = mode
path: function_score.go
user: login: bforbhartiii
body: nit: can we make the arg name same as the field name to which we are assigning this?
mode -> boostMode renaming in this case
created_at: 2025-05-13T07:25:56Z
author_association: NONE
diff_hunk: @@ -0,0 +1,104 @@
+// Package osquery Modified by harshit98 on 2025-05-10
+// Changes: Added function score support
+package osquery
+
+type FunctionScoreQuery struct {
+	query     Mappable
+	functions []Function
+	boostMode string
+	scoreMode string
+	maxBoost  float32
+	minScore  float32
+	boost     float32
+}
+
+type Function interface {
+	Map() map[string]interface{}
+}
+
+type RandomScoreFunction struct {
+	seed  *int64
+	field string
+}
+
+type FieldValueFactorFunction struct {
+	field    string
+	factor   float32
+	modifier string
+	missing  float32
+}
+
+// Additional functions can be added as per usecase in future like:
+// - ScriptScoreFunction
+// - DecayFunction (with variants for geo, date, numeric)
+// - WeightFunction
+//
+// For ref: https://docs.opensearch.org/docs/latest/query-dsl/compound/function-score/
+
+func FunctionScore(query Mappable) *FunctionScoreQuery {
+	return &FunctionScoreQuery{query: query}
+}
+
+func (q *FunctionScoreQuery) Function(f Function) *FunctionScoreQuery {
+	q.functions = append(q.functions, f)
+	return q
+}
+
+func (q *FunctionScoreQuery) BoostMode(mode string) *FunctionScoreQuery {
+	q.boostMode = mode
+	return q
+}
+
+func (q *FunctionScoreQuery) Map() map[string]interface{} {
+	m := make(map[string]interface{})
+
+	if q.query != nil {
+		m["query"] = q.query.Map()
+	}
+
+	if len(q.functions) > 0 {
+		funcs := make([]map[string]interface{}, len(q.functions))
+		for i, f := range q.functions {
+			funcs[i] = f.Map()
+		}
+		m["functions"] = funcs
+	}
+
+	if q.boostMode != "" {
+		m["boost_mode"] = q.boostMode
+	}
+
+	return map[string]interface{}{
+		"function_score": m,
+	}
+}
path: function_score.go
user: login: bforbhartiii
body: SAA, need handling of scoreMode, maxBoost, minScore and score
created_at: 2025-05-13T07:26:27Z
author_association: NONE
diff_hunk: @@ -0,0 +1,104 @@
+// Package osquery Modified by harshit98 on 2025-05-10
+// Changes: Added function score support
+package osquery
+
+type FunctionScoreQuery struct {
+	query     Mappable
+	functions []Function
+	boostMode string
+	scoreMode string
+	maxBoost  float32
+	minScore  float32
+	boost     float32
+}
+
+type Function interface {
+	Map() map[string]interface{}
+}
+
+type RandomScoreFunction struct {
path: function_score.go
user: login: bforbhartiii
body: usage of this in FunctionScoreQuery is missing - both in struct and assignment part in Map() function
created_at: 2025-05-13T07:32:46Z
author_association: NONE
diff_hunk: @@ -0,0 +1,104 @@
+// Package osquery Modified by harshit98 on 2025-05-10
+// Changes: Added function score support
+package osquery
+
+type FunctionScoreQuery struct {
+	query     Mappable
+	functions []Function
+	boostMode string
+	scoreMode string
+	maxBoost  float32
+	minScore  float32
+	boost     float32
path: function_score.go
user: login: bforbhartiii
body: script score missing
created_at: 2025-05-13T07:37:45Z
author_association: NONE
diff_hunk: @@ -0,0 +1,104 @@
+// Package osquery Modified by harshit98 on 2025-05-10
+// Changes: Added function score support
+package osquery
+
+type FunctionScoreQuery struct {
+	query     Mappable
+	functions []Function
+	boostMode string
+	scoreMode string
+	maxBoost  float32
+	minScore  float32
+	boost     float32
+}
+
+type Function interface {
+	Map() map[string]interface{}
+}
+
+type RandomScoreFunction struct {
path: function_score.go
user: login: harshit98
body: `RandomScoreFunction` is being used in the test case, please check. It will be used in similar fashion when creating query in collection products if filter type received will be `sort: "random"`
created_at: 2025-05-13T12:14:55Z
author_association: MEMBER

PR comments:
user: login: bforbhartiii
created_at: 2025-05-13T07:38:26Z
body: need to update test cases accordingly
author_association: NONE
user: login: bforbhartiii
created_at: 2025-05-13T12:57:15Z
body: function_score me base level pe random_score and script_score we need, that is missing
author_association: NONE
user: login: harshit98
created_at: 2025-05-14T06:35:05Z
body: Discussed offline, PR good to go. Merging.
author_association: MEMBER