func createSpanNearQuery(field string, term string, boost float32, slop int, inOrder bool) (elastic.Query, error) {
	clauses := make([]string, 0)
	spanNearMask := `{"span_near": { "clauses": [%s], "slop": %d, "boost": %f, "in_order": %t } }`
	clauseMask := `{"span_multi": { "match": { "fuzzy": { "%s": { "value": "%s", "fuzziness": %s, "transpositions": %s } } } } }`
	splitted := strings.Fields(term)
	for _, t := range splitted {
		if t == "<" || t == ">" || t == "-" {
			continue
		}
		fuzzines := `"AUTO"`      
		transpositions := "true"  
		runes := []rune(t)
		_, convertToIntErr := strconv.Atoi(t)
		if convertToIntErr == nil || (len(runes) == 3 && runes[1] == '"') || (len(runes) == 4 && runes[2] == '"') {
			fuzzines = "0"
		} else if len(runes) == 1 && runes[0] >= 'א' && runes[0] <= 'ת' {
			fuzzines = "1"
			transpositions = "false"  
		}
		b, err := json.Marshal(t)
		if err != nil {
			return nil, errors.Wrap(err, "createSpanNearQuery")
		}
		esc := string(b[1 : len(b)-1])
		clause := fmt.Sprintf(clauseMask, field, esc, fuzzines, transpositions)
		clauses = append(clauses, clause)
	}
	clausesStr := strings.Join(clauses, ",")
	queryStr := fmt.Sprintf(spanNearMask, clausesStr, slop, boost, inOrder)
	query := elastic.NewRawStringQuery(queryStr)
	return query, nil
}
Here we build the span_near query with span_multi sub queries to allow effective fuzzy search
with slop and special cases like avoiding numeric values from applying fuzziness and handling of single hebrew letter in the query.
The query is not supported in the elastic SDK so we build it manually.
Arguments:
field - the field where we search (title, full_title, description, content).
term - the search term.
boost - boost for the score.
slop - number of words separating the span clauses.
inOrder - we expect the words to be in order? We set it as true only for the search in title.
package search
import (
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
	"unicode"
	"gopkg.in/olivere/elastic.v6"
	"github.com/Bnei-Baruch/archive-backend/consts"
	"github.com/Bnei-Baruch/archive-backend/es"
	"github.com/Bnei-Baruch/archive-backend/utils"
	"github.com/pkg/errors"
)
const (
	TITLE_BOOST       = 2.0
	DESCRIPTION_BOOST = 1.2
	FULL_TITLE_BOOST  = 1.1
	DEFAULT_BOOST     = 1.0
	SLOP = 100
	STANDARD_BOOST = 1.2
	EXACT_BOOST = 1.5
	SPAN_NEAR_BOOST = 0.01
	MIN_SCORE_FOR_RESULTS = 0.01
	NUM_SUGGESTS = 30
)
type Query struct {
	Term          string              `json:"term,omitempty"`
	ExactTerms    []string            `json:"exact_terms,omitempty"`
	Original      string              `json:"original,omitempty"`
	Filters       map[string][]string `json:"filters,omitempty"`
	LanguageOrder []string            `json:"language_order,omitempty"`
	Deb           bool                `json:"deb,omitempty"`
	Intents       []Intent            `json:"intents,omitempty"`
}
func isTokenStart(i int, runes []rune, lastQuote rune) bool {
	return i == 0 && !unicode.IsSpace(runes[0]) ||
		(i > 0 && !unicode.IsSpace(runes[i]) && unicode.IsSpace(runes[i-1]))
}
func isTokenEnd(i int, runes []rune, lastQuote rune, lastQuoteIdx int) bool {
	return i == len(runes)-1 ||
		(i < len(runes)-1 && unicode.IsSpace(runes[i+1]) &&
			(lastQuote == rune(0) || runes[i] == lastQuote && lastQuoteIdx >= 0 && lastQuoteIdx < i))
}
func isRuneQuotationMark(r rune) bool {
	return unicode.In(r, unicode.Quotation_Mark) || r == rune(1523) || r == rune(1524)
}
var SINGLE_QUOTE_RUNES = map[rune]bool{
	rune(39):   true,
	rune(145):  true,
	rune(146):  true,
	rune(8216): true,
	rune(8217): true,
	rune(8219): true,
	rune(1523): true,
	rune(1436): true,
	rune(1437): true,
}
func doubleSingleQuotesToDoubleQuotes(runes []rune) []rune {
	ret := []rune(nil)
	i := 0
	for ; i < len(runes)-1; i++ {
		_, quoteFirst := SINGLE_QUOTE_RUNES[runes[i]]
		_, quoteSecond := SINGLE_QUOTE_RUNES[runes[i+1]]
		if quoteFirst && quoteSecond {
			ret = append(ret, rune(34))
			i++
		} else {
			ret = append(ret, runes[i])
		}
	}
	if i < len(runes) {
		ret = append(ret, runes[i])
	}
	return ret
}
func tokenize(str string) []string {
	runes := doubleSingleQuotesToDoubleQuotes([]rune(str))
	start := -1
	lastQuote := rune(0)
	lastQuoteIdx := -1
	parts := 0
	var tokens []string
	for i, r := range runes {
		if start == -1 && isTokenStart(i, runes, lastQuote) {
			start = i
		}
		if i == start && lastQuote == rune(0) && isRuneQuotationMark(r) {
			for k := i + 1; k < len(runes); k++ {  
				if isTokenEnd(k, runes, r, i) && isRuneQuotationMark(runes[k]) {
					lastQuote = r
					lastQuoteIdx = i
					break
				}
			}
		}
		if start >= 0 && isTokenEnd(i, runes, lastQuote, lastQuoteIdx) {
			tokens = append(tokens, string(runes[start:i+1]))
			lastQuote = rune(0)
			lastQuoteIdx = -1
			start = -1
			parts += 1
		}
	}
	return tokens
}
func ParseQuery(q string) Query {
	filters := make(map[string][]string)
	var terms []string
	var exactTerms []string
	for _, t := range tokenize(q) {
		isFilter := false
		for filter := range consts.FILTERS {
			prefix := fmt.Sprintf("%s:", filter)
			if isFilter = strings.HasPrefix(t, prefix); isFilter {
				filters[consts.FILTERS[filter]] = strings.Split(strings.TrimPrefix(t, prefix), ",")
				break
			}
		}
		if !isFilter {
			runes := []rune(t)
			if len(runes) >= 2 && isRuneQuotationMark(runes[0]) && runes[0] == runes[len(runes)-1] {
				exactTerms = append(exactTerms, string(runes[1:len(runes)-1]))
			} else {
				terms = append(terms, t)
			}
		}
	}
	return Query{Term: strings.Join(terms, " "), ExactTerms: exactTerms, Original: q, Filters: filters}
}