Custom SearchProvider with parameters?

Hey,

I’m trying to create a custom SearchProvider that has an input parameter, like for example the built-in “Rendering Layers” one, which lets you choose between predefined values.

How do you create something like that?
The documentation only gives a very basic example without parameters but I’m unable to find more advanced examples and it seems like the package source code is not available anymore so I can’t check how the built-in providers work.
I found QueryListBlock which sounds like it might be want I need but I can’t figure out how to use it.

Hi @Sebioff ,

Sorry for the delay in answering your questions :frowning:

Since a code example is worth a thousand words here is an example of the QuerListBlock in action for labels:

[QueryListBlock("Labels", "label", "l", ":")]
class QueryLabelBlock : QueryListBlock
{
    static readonly Texture2D kLabelIcon = Utils.LoadIcon("QuickSearch/AssetLabelIconSquare");
    public static Texture2D GetLabelIcon()
    {
        return kLabelIcon;
    }

    public QueryLabelBlock(IQuerySource source, string id, string value, QueryListBlockAttribute attr)
        : base(source, id, value, attr)
    {
        icon = GetLabelIcon();
    }

    public override IEnumerable<SearchProposition> GetPropositions(SearchPropositionFlags flags)
    {
        foreach (var l in AssetDatabase.GetAllLabels())
        {
            yield return CreateProposition(flags, ObjectNames.NicifyVariableName(l.Key), l.Key, $"Assets with label: {l.Key}");
        }
    }
}

Basically you specify the SearchPropositions that should be available in the drop down of the query block. This block will be create if the l filter is used.

If you want to populate the “QueryBuilder + button” with your various choices you will need to ensure your custom SearchProvider has a fetchPropositions callback and that this callbacks uses the QueryBlock to generates its propositions:

static IEnumerable<SearchProposition> FetchPropositions(SearchContext context, SearchPropositionOptions options)
{
    if (!options.flags.HasAny(SearchPropositionFlags.QueryBuilder))
        yield break;

    foreach (var f in QueryListBlockAttribute.GetPropositions(typeof(QueryTypeBlock)))
        yield return f;
    foreach (var f in QueryListBlockAttribute.GetPropositions(typeof(QueryLabelBlock)))
        yield return f;
    foreach (var f in QueryListBlockAttribute.GetPropositions(typeof(QueryAreaFilterBlock)))
        yield return f;
    foreach (var f in QueryListBlockAttribute.GetPropositions(typeof(QueryBundleFilterBlock)))
        yield return f;

    yield return new SearchProposition(category: null, "Reference", "ref:<$object:none,UnityEngine.Object$>", "Find all assets referencing a specific asset.");
    yield return new SearchProposition(category: null, "Glob", "glob:\"Assets/**/*.png\"", "Search according to a glob query.");
}

[SearchItemProvider]
internal static SearchProvider CreateProvider()
{
    return new SearchProvider(type, "Asset Database")
    {
        type = "asset",
        active = false,
        priority = 2500,
        fetchItems = (context, items, provider) => FetchItems(context, SearchService.GetProvider("asset") ?? provider),
        fetchPropositions = (context, options) => FetchPropositions(context, options)
    };
}

If I may ask:

  • Why are you creating a new SearchProvider? For which workflows?
  • Which items will you be yielding?

I like to know when our clients need to define their own providers. It gives us new ideas on various improvements to the search workflow :slight_smile:

I hope this helps, good luck

Seb

Hey @sebastienp_unity ,
thanks for getting back to me!

  • Why are you creating a new SearchProvider? For which workflows?
  • Which items will you be yielding?

I’m not 100% sure if I need a custom search provider and there might be a better way to do it, but here’s the context and what I’m trying to do:
We’re creating a RTS-type game that has many resources players need to gather. These resources can be used for various things such as construction materials, or as inputs for production chains, so there are various fields in our prefabs that reference these resources.
I’m trying to give our designers a quick and easy way to search through all of our prefabs for any use of a specific resource.

This can probably be achieved with the existing search features but I thought the friendliest way for our designers would be if they just have to click the “resource uses” search provider in the search window and then they just have to pick the resource they want to search for and that’s it.

For your code example:
I think that’s pretty much what I’m doing but I can’t get the drop down to show up on the query block.
There’s some changes I had to make to your example because unfortunately QueryListBlockAttribute.GetPropositions and QueryListBlock.CreateProposition are internal. From what I saw though they don’t seem to be doing anything super special.

Here’s what I got:

private class ResourceUsesSearchProvider : SearchProvider {
	private readonly QueryEngine<AbstractConfiguredObjectAuthoring> queryEngine = new();

	private readonly List<ResourceFilter> resourceFilters = new();

	private readonly struct ResourceFilter {
		public readonly ResourceType resourceType;

		public ResourceFilter(ResourceType resourceType) {
			this.resourceType = resourceType;
		}

		public bool containsResourceType(AbstractConfiguredObjectAuthoring abstractConfiguredObjectAuthoring) {
			// ...
			return false;
		}
	}

	public ResourceUsesSearchProvider(string id, string displayName) : base(id, displayName) {
		foreach (ResourceType value in Enum.GetValues(typeof(ResourceType))) {
			resourceFilters.Add(new ResourceFilter(value));
		}

		foreach (ResourceFilter resourceFilter in resourceFilters) {
			queryEngine.AddFilter(resourceFilter.resourceType.ToString(), resourceFilter.containsResourceType);
		}

		SearchValue.SetupEngine(queryEngine);

		fetchItems = (context, items, provider) => FetchItems(context, provider);
		fetchPropositions = FetchPropositions;
	}

	IEnumerable<SearchProposition> FetchPropositions(SearchContext context, SearchPropositionOptions options) {
		foreach (ResourceFilter resourceFilter in resourceFilters) {
			yield return new SearchProposition(category: "Resource Type", label: resourceFilter.resourceType.ToString(), resourceFilter.resourceType.ToString());
		}
	}

	IEnumerable<SearchItem> FetchItems(SearchContext context, SearchProvider provider) {
		if (context.empty)
			yield break;

		if (context.filterId != provider.filterId) {
			yield break;
		}

		var query = queryEngine.ParseQuery(context.searchQuery);

		using (var meshResults = SearchService.Request($"t:{nameof(AbstractConfiguredObjectAuthoring)}")) {
			foreach (SearchItem result in meshResults) {
				if (result == null) {
					yield return null;
				}
				else {
					AbstractConfiguredObjectAuthoring prefab = result.ToObject<AbstractConfiguredObjectAuthoring>();
					if (prefab == null) {
						Debug.LogWarning($"Invalid prefab: {result.id}");
						continue;
					}

					bool isValid = query.Test(prefab);

					if (isValid) {
						yield return provider.CreateItem(context, AssetDatabase.GetAssetPath(prefab), null, null, null, null);
					}
				}
			}
		}
	}
}

[QueryListBlock("Resource Uses", "resource_uses", "resource_uses", ":")]
class QueryLabelBlock : QueryListBlock {
	public QueryLabelBlock(IQuerySource source, string id, string value, QueryListBlockAttribute attr)
		: base(source, id, value, attr) {
	}

	public override IEnumerable<SearchProposition> GetPropositions(SearchPropositionFlags flags) {
		foreach (ResourceType value in Enum.GetValues(typeof(ResourceType))) {
			yield return new SearchProposition(category: "Resource Type", label: value.ToString(), value.ToString());
		}
	}
}

[SearchItemProvider]
internal static SearchProvider CreateProvider() {
	return new ResourceUsesSearchProvider("resource_uses", "Resource Uses") {
		priority = 99999, // Put example provider at a low priority
		showDetailsOptions = ShowDetailsOptions.Inspector | ShowDetailsOptions.Actions | ShowDetailsOptions.Preview,
		fetchThumbnail = (item, context) => AssetDatabase.GetCachedIcon(item.id) as Texture2D,
		fetchPreview = (item, context, size, options) => AssetDatabase.GetCachedIcon(item.id) as Texture2D,
		fetchLabel = (item, context) => AssetDatabase.LoadMainAssetAtPath(item.id)?.name,
		fetchDescription = (item, context) => item.id,
		toObject = (item, type) => AssetDatabase.LoadMainAssetAtPath(item.id),
		trackSelection = TrackSelection,
		startDrag = StartDrag,
	};
}

I’m not entirely sure what the catgegory and name fields on QueryListBlock are so maybe I’ve got something wrong in there and that’s why it’s not showing up?

Hi @Sebioff ,

I think I understand correctly our workflow. And I think I have a better suggestion for you instead of creating a new SearchProvider.

We have a functionality in the called “SearchQueries”. This means you can write a query (as complex and precise as you like), setup the Search Window view state in the way you like (Grid, List, Table) and you can save that query on disk as an asset. You can then either double click the query to open a Search Window in the same state. Or use the Query Panel to load the query. This would allow you to create essentially “dynamic collections” or palettes of prefabs for level editing. You could save a different query for each category of resources/buildings/units in your game.

I have written a wiki entry about this workflow:

This could give you something like this:

The SearchQueryAsset Inspector even shows a preview of the query results:

Also while being at it: the Search Window has a few workflows that are “hidden” in right click menu. If you right click on an asset, you can select “Find Reference In project”. This will open the Seach Window searching for all assets referencing the selected asset.

If you right click on a properties in the Inspector, you can select “Find Same value” This will search all objects on your current scene or project with the same property value. If you right click on a property of “ObjectReference” type you can also search for all objects referencing the object.

Some nice gifs of these workflows in actions can be seen here: Search Integration · Unity-Technologies/com.unity.search.extensions Wiki · GitHub

As for creating a new SearchProvider:

  • I feel you should create a new SearchProvider only if you need to “search” for items are are not already covered by a provider. I think that any “asset” search would be better done with the current Asset Provider mixed with SearchQueryAsset to remeber your most imporant query.
  • As an example some internal teams at unity have created SearchProvider for “graph nodes” in the currently loaded Graph. Searching for nodes wasn’t covered by our current list of builtin providers (Hierarchy, Assets, Settings, menus…).
  • That said if you prefer to create your own search provider for your specific use case, I can eventually have a look at your code.

Hope this helps,

Sebastien

Hey @sebastienp_unity ,

thanks for the additional info!
I was aware of saving queries but didn’t know about the table view, that’s really nice and I’m sure we’ll find a use for that.
I wasn’t aware that you could search for a specific property value, that’s quite nice too - however I don’t think this covers my use case.

When I use “Search Same Property Value” it searches for objects that have the selected value on the same exact property as selected.
What I’m looking for is a search that gives me objects with the selected value but across any of the properties of the object. Basically the same you get when searching for an ObjectReference type property, but without it being an ObjectReference property.
Is there such a thing?

Hi @Sebioff,

From your last post I think you need the ref:<path to an asset>

This will yield all objects who are referencing path to an asset regardless of property usage. References are the only property type we index “globally” through the ref filter.

You can build a ref query using the Query Builder:

You can right click on an asset and select Find Reference in Project:

If you right click on an object reference properties you can click Find Reference to:

I hope this helps but If I misunderstood your explanation can you give me a more concrete example of what you are looking to do?

Seb

No that’s not what I’m looking for - I’ll try to explain with a concrete example:

Let’s say I have an enum that defines the resource types that are available in the game:

public enum ResourceType {
	Wood,
	Stone,
}

And then I have some MonoBehaviours with various properties of type ResourceType, for example:

public class ResourceUser : MonoBehaviour {
	public ResourceType requiredResource;
}

public class ConstructedObject : MonoBehaviour {
	public ResourceType[] requiredConstructionMaterials;
}

public class ResourceProducer : MonoBehaviour {
	public ProductionRecipe[] recipes;

	public struct ProductionRecipe {
		public ResourceType[] requiredResources;
		public ResourceType[] producedResources;
	}
}

With the example you previously gave with “Find Same value” I could do a query that checks a single specific property, e.g:

p:ResourceUser.requiredResource=Wood

But I want to find all GameObjects that use a specific resource type (for example ResourceType.Wood) in any property of any MonoBehaviour (without having to build a giant query where I manually list all the properties that might be possible since in our actual project that would be an unmanageably large amount of MonoBehaviours + properties; and additionally this manual list would be quite error prone since it can change at any time).

So I want something that kinda behaves like ref, except for a property value instead of a referenceable object.

Hi @Sebioff,

I finally understand! :slight_smile: Sorry for the comprehension delay.

You could implement this specific feature for assets using a CustomIndexer. Basically this is what you want to do:

  1. Create a CustomObjectIndexer for each types containing ResourceType (or create a CustomObjectIndexer for all types and use reflection to see if this object has properties using ResourceType (though this last solution would make the indexing workflow slower).
  2. Use the ObjectIndexer pass to the CustomObjectIndexer to add a resourceTypeRef properties for all these objects (similar to what we do with ref).
  3. When querying using either the searchWindow or the SearchService API you could write
    resourceTypeRef=Wood

I found your example to be interesting so I implemented it in our Search Extensions Package.

  • Here is the link to the CustomIndexer code.
  • Here is an article describing the process of CustomIndexing in greater details. There is some tips on debuggin a custom indexers and keeping increasing your version of customindexer.

A few pictures of this new filter in action:

Hey,

that sounds like a good approach!

But I’m having trouble getting it working :frowning:

I’ve copied your CustomIndexer that you linked to without any changes

In this screenshot you can see:

  1. I have a prefab with the ResourceUser component and required resource = Wood
  2. The custom indexer does seem to get picked up, it’s included in the custom indexer list in the preferences dialog. When changing the version number of the custom indexer the asset index is being updated, so it seems to be doing something.
  3. However, I do not get any results when searching for resourcetyperef=Wood

I looked at the Index Manager window and I assume it should list resourcetyperef there on the Keywords tab? But it doesn’t:

I also saw the section about testing custom indexers with unit tests in the article you linked, and saw you added a test case in the search extensions package.
Putting this test case into my project also fails:

I’ve tried rebuilding the search index but it made no difference.

I’m not sure if the “Properties” option in the index manager window needs to be on or not?
The tooltip talks about serialized properties specifically and the article you linked said a custom indexer is a good alternative to having to index all the serialized properties so it sounds like it’s not necessary? But since the custom indexer indexes properties I turned it on anyways to be safe (didn’t make a difference though):

I’m using Unity 6000.0.24f1 in case that makes any difference

Hey @Sebioff,

I tested all this workflow with 6.2 (our internal developement version). There might be stuff missing in 6.0 preventing this thing to properly work.

  1. I will test with 6.0 asap and report back to you.
  2. If indexing/search fixes are missing from 6.0 I will create the proper bug backport.

In all cases I will keep you posted.

Thanks for your patience and your feedback.

Seb

Ok @Sebioff

I have news: I tested the Search Extensions package with various versions:

  • 6.0.0.48f (the next version of 6 that will be promoted soonish): Everything works out of the box.
  • 6.0.0.46f : Everything works out of the box.
  • 6.0.0.24f : No results are found with resourcetyperef=Stone

If your team updates to the highest 6.0 things should work. But I know that for some project it is risky to update mid production. If I have time today, I will look at the CustomResourceIndexing.cs code to see if we can fix something quickly to make it work for your cut of Unity.

I will keep you posted,

Seb

Hi @Sebioff,

After having tested your version of ?Unity (6.0.024f) I am sad to report that this version didn’t supported CustomIndexer for prefab components. This means there is nothing you can do to make it work for prefab. That said if your project uses ScriptableObject to store ResourceType this will work. I added a new sample to Search Extensions to show ScriptableObject custom indexation.

The issue was fixed in a newer version of 6.0.x (not sure which though). If you upgrade to the latest Unity 6 (6.0.0.46f) the samples in search.extensions will all pass.

Sorry,

Seb

That’s fine, we’ll update to the latest Unity 6 version then.

Thank you so much for all your help with figuring out how to get this working and testing everything, that’s really appreciated!

If I may make one last suggestion:
I think the search section in the Unity documentation could really use an update to surface all of this better and to make it clear when to use a custom search provider vs a custom indexer.

I.e. if you look at the Search section it only talks about search providers; in the search provider section it says:

A Search Provider allows you to search and filter content

And then there’s a whole section about how to create your own custom search provider.

From what I can see custom indexers are not being mentioned anywhere.
All of this makes it sound like custom search providers are the way to go (and the only option really) to do what I wanted to do.

It’s really nice that this search system is so extendable, but I think it could be more clear how to do it.

Ok we’ve updated to Unity 6000.0.47f1 and it’s mostly working, but the automatic search index update isn’t working correctly.

When adding a new CustomObjectIndexer or changing the version number of an existing CustomObjectIndexer a search index update seems to get triggered (the background task for updating the search index appears in the bottom right corner of the Unity editor).

However, objects only appear in the search results after manually triggering a full rebuild of the search index.

Hey @Sebioff

You are right that our Doc (especially the one in the Unity Manual) is lacking. I am not a Doc writer and a lot of the Search customization workflows are pretty involved in terms of coding. This is the main reason I started to right a bunch of tutorials and articles on the Search Extensions Wiki.

The ultimate goal is for our Doc Writer to integrate these articles in the official Manual. Hopefully this can happens soon(ish).

I want to clarify for the issue you are currently having:

  • When modifying your CustomObjectIndex and incrementing the version no indexing was triggered?
  • When adding a new customObjectIndexer no indexing was trigger?

In both of these cases you had to Force Reindexing?

From memory we have fixed this issues in the past few months. My guess is that the fixes were not backported to the U6.0.X stream.

Keep me posted on your usage of CustomObjectIndexer and your possible usage of the Table View to allow easier editing of gameplay values.

Seb

For both cases it does seem to trigger the indexing (the indexing background task starts and does something), but it doesn’t add anything new to the index.

Correct

Sure! I tried the table view today but the option to make properties editable doesn’t seem to exist in our Unity version yet. I’ll give it another look once Unity 6.1 is out of beta.

Hey @Sebioff

You should be able to make the columns editable. By default they are read only. But if you right click on the column, select Column Format → Serialized Property this will make the column editable through the SerializedProperty protocol (if the column maps on a property of course).

Seb